1#![allow(clippy::empty_line_after_doc_comments)]
2
3use crate::engine::bt_upload_session::PieceDataProvider;
4use crate::engine::multi_file_layout::MultiFileLayout;
5use crate::error::{Aria2Error, FatalError, Result};
6use std::collections::{HashMap, HashSet};
7use std::time::Instant;
8
9#[derive(Debug, Clone)]
50pub struct PieceDownloadState {
51 pub piece_index: u32,
53 pub total_blocks: u32,
55 pub completed_blocks: HashSet<u32>,
57 pub requested_blocks: HashMap<u32, Instant>,
59 pub last_activity: Instant,
61}
62
63impl PieceDownloadState {
64 pub fn new(piece_index: u32, piece_length: u32, block_size: u32) -> Self {
74 let total_blocks = if block_size > 0 {
75 piece_length.div_ceil(block_size)
76 } else {
77 0
78 };
79
80 Self {
81 piece_index,
82 total_blocks,
83 completed_blocks: HashSet::new(),
84 requested_blocks: HashMap::new(),
85 last_activity: Instant::now(),
86 }
87 }
88
89 pub fn blocks_remaining(&self) -> usize {
94 (self.total_blocks as usize).saturating_sub(self.completed_blocks.len())
95 }
96
97 pub fn is_complete(&self) -> bool {
102 self.completed_blocks.len() as u32 >= self.total_blocks
103 }
104
105 pub fn is_stalled(&self, timeout_secs: u64) -> bool {
118 !self.is_complete()
119 && self.last_activity.elapsed().as_secs() > timeout_secs
120 && !self.requested_blocks.is_empty()
121 }
122
123 pub fn mark_block_requested(&mut self, block_index: u32) {
130 self.requested_blocks.insert(block_index, Instant::now());
131 self.last_activity = Instant::now();
132 }
133
134 pub fn mark_block_received(&mut self, block_index: u32) {
141 self.completed_blocks.insert(block_index);
142 self.requested_blocks.remove(&block_index);
143 self.last_activity = Instant::now();
144 }
145
146 pub fn mark_block_cancelled(&mut self, block_index: u32) {
154 self.requested_blocks.remove(&block_index);
155 }
156
157 pub fn progress_percent(&self) -> f64 {
162 if self.total_blocks == 0 {
163 return 100.0;
164 }
165 (self.completed_blocks.len() as f64 / self.total_blocks as f64) * 100.0
166 }
167}
168
169pub struct FileBackedPieceProvider {
177 file_path: std::path::PathBuf,
178 piece_length: u32,
179 num_pieces: u32,
180 multi_file_layout: Option<MultiFileLayout>,
181}
182
183impl FileBackedPieceProvider {
184 pub fn new(
185 file_path: std::path::PathBuf,
186 piece_length: u32,
187 num_pieces: u32,
188 multi_file_layout: Option<MultiFileLayout>,
189 ) -> Self {
190 Self {
191 file_path,
192 piece_length,
193 num_pieces,
194 multi_file_layout,
195 }
196 }
197}
198
199impl PieceDataProvider for FileBackedPieceProvider {
200 fn get_piece_data(&self, piece_index: u32, offset: u32, length: u32) -> Option<Vec<u8>> {
201 use std::io::SeekFrom;
202 use tokio::fs::File;
203 use tokio::io::{AsyncReadExt, AsyncSeekExt};
204
205 let read_op =
206 move |file_path: std::path::PathBuf, seek_pos: u64, len: u32| -> Option<Vec<u8>> {
207 let rt = match tokio::runtime::Handle::try_current() {
208 Ok(handle) => handle,
209 Err(_) => {
210 let rt = tokio::runtime::Runtime::new().ok()?;
211 return rt.block_on(async {
212 let mut f = File::open(&file_path).await.ok()?;
213 f.seek(SeekFrom::Start(seek_pos)).await.ok()?;
214 let mut buf = vec![0u8; len as usize];
215 f.read_exact(&mut buf).await.ok()?;
216 Some(buf)
217 });
218 }
219 };
220 tokio::task::block_in_place(|| {
221 rt.block_on(async {
222 let mut f = File::open(&file_path).await.ok()?;
223 f.seek(SeekFrom::Start(seek_pos)).await.ok()?;
224 let mut buf = vec![0u8; len as usize];
225 f.read_exact(&mut buf).await.ok()?;
226 Some(buf)
227 })
228 })
229 };
230
231 if let Some(ref layout) = self.multi_file_layout {
232 let global_start = piece_index as u64 * layout.piece_length() as u64 + offset as u64;
233
234 if global_start >= layout.total_size() {
235 return None;
236 }
237
238 let actual_length = (length as u64).min(layout.total_size() - global_start) as u32;
239 let mut result = Vec::with_capacity(actual_length as usize);
240 let mut current_global = global_start;
241 let mut remaining = actual_length as u64;
242
243 while remaining > 0 {
244 let current_piece_idx = (current_global / layout.piece_length() as u64) as u32;
245 let current_offset_in_piece =
246 (current_global % layout.piece_length() as u64) as u32;
247
248 let (file_idx, file_offset) =
249 layout.resolve_file_offset(current_piece_idx, current_offset_in_piece)?;
250 let file_path = layout.file_absolute_path(file_idx)?.to_path_buf();
251
252 let file_info = layout.get_file_info(file_idx)?;
253 let file_end = file_info.start_piece as u64 * layout.piece_length() as u64
254 + file_info.start_offset_in_piece as u64
255 + file_info.length;
256
257 let bytes_available_in_file = file_end - current_global;
258 let bytes_to_read = remaining.min(bytes_available_in_file) as u32;
259
260 let data = read_op(file_path.clone(), file_offset, bytes_to_read)?;
261 result.extend_from_slice(&data);
262 current_global += data.len() as u64;
263 remaining -= data.len() as u64;
264 }
265
266 Some(result)
267 } else {
268 let file_pos = piece_index as u64 * self.piece_length as u64 + offset as u64;
269 read_op(self.file_path.clone(), file_pos, length)
270 }
271 }
272
273 fn has_piece(&self, _piece_index: u32) -> bool {
274 true
275 }
276
277 fn num_pieces(&self) -> u32 {
278 self.num_pieces
279 }
280}
281
282struct CoalescedWrite {
292 file_idx: usize,
293 file_offset: u64,
294 data: Vec<u8>,
295}
296
297const COALESCE_GAP: u64 = 4096;
301
302pub async fn write_piece_to_multi_files(
316 layout: &MultiFileLayout,
317 piece_idx: u32,
318 piece_data: &[u8],
319 _piece_length: u32,
320) -> Result<()> {
321 use tokio::io::{AsyncSeekExt, AsyncWriteExt};
322
323 let mut file_writers: HashMap<usize, tokio::fs::File> = HashMap::new();
324
325 let mut data_offset = 0usize;
326 while data_offset < piece_data.len() {
327 let piece_offset = data_offset as u32;
328
329 if let Some((file_idx, file_offset)) = layout.resolve_file_offset(piece_idx, piece_offset) {
330 let file_path = layout
331 .file_absolute_path(file_idx)
332 .ok_or_else(|| {
333 Aria2Error::Fatal(FatalError::Config("invalid file index".to_string()))
334 })?
335 .to_path_buf();
336
337 if let std::collections::hash_map::Entry::Vacant(e) = file_writers.entry(file_idx) {
338 let f = tokio::fs::OpenOptions::new()
339 .create(true)
340 .truncate(true)
341 .write(true)
342 .open(&file_path)
343 .await
344 .map_err(|e| {
345 Aria2Error::Fatal(FatalError::Config(format!("open failed: {}", e)))
346 })?;
347 e.insert(f);
348 }
349
350 let file_info = layout.get_file_info(file_idx).ok_or_else(|| {
351 Aria2Error::Fatal(FatalError::Config("invalid file index".to_string()))
352 })?;
353
354 let bytes_available_in_file = file_info.length.saturating_sub(file_offset);
355 let bytes_remaining_in_piece = (piece_data.len() - data_offset) as u64;
356 let write_len = bytes_available_in_file.min(bytes_remaining_in_piece) as usize;
357
358 if write_len == 0 {
359 break;
360 }
361
362 let chunk = &piece_data[data_offset..data_offset + write_len];
363
364 let writer = file_writers.get_mut(&file_idx).unwrap();
365 writer
366 .seek(std::io::SeekFrom::Start(file_offset))
367 .await
368 .map_err(|e| {
369 Aria2Error::Fatal(FatalError::Config(format!("seek failed: {}", e)))
370 })?;
371 writer.write_all(chunk).await.map_err(|e| {
372 Aria2Error::Fatal(FatalError::Config(format!("write failed: {}", e)))
373 })?;
374
375 data_offset += write_len;
376 } else {
377 break;
378 }
379 }
380
381 for (_, mut f) in file_writers {
382 f.flush()
383 .await
384 .map_err(|e| Aria2Error::Fatal(FatalError::Config(format!("flush failed: {}", e))))?;
385 }
386
387 Ok(())
388}
389
390pub async fn write_piece_to_multi_files_coalesced(
422 layout: &MultiFileLayout,
423 piece_idx: u32,
424 piece_data: &[u8],
425 _piece_length: u32,
426) -> Result<()> {
427 use tokio::io::{AsyncSeekExt, AsyncWriteExt};
428
429 let mut raw_writes: Vec<(usize, u64, Vec<u8>)> = Vec::new();
433 let mut data_offset = 0usize;
434
435 while data_offset < piece_data.len() {
436 let piece_offset = data_offset as u32;
437 if let Some((file_idx, file_offset)) = layout.resolve_file_offset(piece_idx, piece_offset) {
438 let file_info = layout.get_file_info(file_idx).ok_or_else(|| {
439 Aria2Error::Fatal(FatalError::Config("invalid file index".to_string()))
440 })?;
441
442 let bytes_available = file_info.length.saturating_sub(file_offset);
443 let bytes_remaining = (piece_data.len() - data_offset) as u64;
444 let write_len = bytes_available.min(bytes_remaining) as usize;
445
446 if write_len > 0 {
447 raw_writes.push((
448 file_idx,
449 file_offset,
450 piece_data[data_offset..data_offset + write_len].to_vec(),
451 ));
452 data_offset += write_len;
453 } else {
454 break;
455 }
456 } else {
457 break;
458 }
459 }
460
461 raw_writes.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
465
466 let mut coalesced: Vec<CoalescedWrite> = Vec::new();
470
471 for (file_idx, file_offset, data) in raw_writes {
472 if let Some(last) = coalesced.last_mut() {
473 let last_end = last.file_offset + last.data.len() as u64;
474 if last.file_idx == file_idx && file_offset <= last_end + COALESCE_GAP {
475 if file_offset > last_end {
477 last.data
479 .extend_from_slice(&vec![0u8; (file_offset - last_end) as usize]);
480 }
481 last.data.extend_from_slice(&data);
482 continue;
483 }
484 }
485 coalesced.push(CoalescedWrite {
486 file_idx,
487 file_offset,
488 data,
489 });
490 }
491
492 let mut file_writers: HashMap<usize, tokio::fs::File> = HashMap::new();
496
497 for cw in &coalesced {
498 let file_path = layout
499 .file_absolute_path(cw.file_idx)
500 .ok_or_else(|| Aria2Error::Fatal(FatalError::Config("invalid file index".to_string())))?
501 .to_path_buf();
502
503 let writer = match file_writers.entry(cw.file_idx) {
504 std::collections::hash_map::Entry::Vacant(e) => {
505 let f = tokio::fs::OpenOptions::new()
506 .create(true)
507 .truncate(true)
508 .write(true)
509 .open(&file_path)
510 .await
511 .map_err(|e| {
512 Aria2Error::Fatal(FatalError::Config(format!("open failed: {}", e)))
513 })?;
514 e.insert(f)
515 }
516 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
517 };
518
519 writer
520 .seek(std::io::SeekFrom::Start(cw.file_offset))
521 .await
522 .map_err(|e| Aria2Error::Fatal(FatalError::Config(format!("seek failed: {}", e))))?;
523 writer
524 .write_all(&cw.data)
525 .await
526 .map_err(|e| Aria2Error::Fatal(FatalError::Config(format!("write failed: {}", e))))?;
527 }
528
529 for (_, mut f) in file_writers {
530 f.flush()
531 .await
532 .map_err(|e| Aria2Error::Fatal(FatalError::Config(format!("flush failed: {}", e))))?;
533 }
534
535 Ok(())
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
543 fn test_piece_download_state_creation() {
544 let state = PieceDownloadState::new(0, 262144, 16384);
546 assert_eq!(state.piece_index, 0);
547 assert_eq!(state.total_blocks, 16); assert!(state.completed_blocks.is_empty());
549 assert!(state.requested_blocks.is_empty());
550 assert!(!state.is_complete());
551 assert_eq!(state.blocks_remaining(), 16);
552 assert_eq!(state.progress_percent(), 0.0);
553 }
554
555 #[test]
556 fn test_piece_download_state_partial_last_block() {
557 let state = PieceDownloadState::new(5, 20000, 16384);
559 assert_eq!(state.total_blocks, 2); assert_eq!(state.blocks_remaining(), 2);
561 }
562
563 #[test]
564 fn test_piece_download_state_zero_block_size() {
565 let state = PieceDownloadState::new(0, 100000, 0);
567 assert_eq!(state.total_blocks, 0);
568 assert!(state.is_complete()); }
570
571 #[test]
572 fn test_mark_block_requested() {
573 let mut state = PieceDownloadState::new(0, 32768, 16384);
574
575 state.mark_block_requested(0);
576 assert_eq!(state.requested_blocks.len(), 1);
577 assert!(state.requested_blocks.contains_key(&0));
578 assert_eq!(state.blocks_remaining(), 2); state.mark_block_requested(1);
581 assert_eq!(state.requested_blocks.len(), 2);
582 }
583
584 #[test]
585 fn test_mark_block_received() {
586 let mut state = PieceDownloadState::new(0, 32768, 16384);
587
588 state.mark_block_requested(0);
590 state.mark_block_received(0);
591
592 assert!(state.completed_blocks.contains(&0));
593 assert!(!state.requested_blocks.contains_key(&0));
594 assert_eq!(state.blocks_remaining(), 1);
595 assert_eq!(state.progress_percent(), 50.0);
596 }
597
598 #[test]
599 fn test_mark_block_cancelled() {
600 let mut state = PieceDownloadState::new(0, 32768, 16384);
601
602 state.mark_block_requested(0);
603 state.mark_block_cancelled(0);
604
605 assert!(!state.completed_blocks.contains(&0));
606 assert!(!state.requested_blocks.contains_key(&0));
607 assert_eq!(state.blocks_remaining(), 2); }
609
610 #[test]
611 fn test_is_complete_all_blocks_received() {
612 let mut state = PieceDownloadState::new(0, 49152, 16384); for i in 0..3 {
615 state.mark_block_requested(i);
616 state.mark_block_received(i);
617 }
618
619 assert!(state.is_complete());
620 assert_eq!(state.blocks_remaining(), 0);
621 assert_eq!(state.progress_percent(), 100.0);
622 }
623
624 #[test]
625 fn test_is_stalled_with_pending_requests() {
626 let mut state = PieceDownloadState::new(0, 32768, 16384);
627
628 state.mark_block_requested(0);
629 state.mark_block_requested(1);
630
631 assert!(!state.is_stalled(30));
633
634 assert!(state.requested_blocks.len() == 2);
638 assert!(!state.is_complete());
639 }
640
641 #[test]
642 fn test_is_not_stalled_when_no_pending_requests() {
643 let mut state = PieceDownloadState::new(0, 32768, 16384);
644
645 state.mark_block_requested(0);
646 state.mark_block_cancelled(0);
647
648 assert!(!state.is_stalled(0)); }
651
652 #[test]
653 fn test_progress_percent_calculation() {
654 let mut state = PieceDownloadState::new(0, 65536, 16384); assert_eq!(state.progress_percent(), 0.0);
657
658 state.mark_block_requested(0);
659 state.mark_block_received(0);
660 assert_eq!(state.progress_percent(), 25.0);
661
662 state.mark_block_requested(1);
663 state.mark_block_received(1);
664 assert_eq!(state.progress_percent(), 50.0);
665
666 state.mark_block_requested(2);
667 state.mark_block_received(2);
668 assert_eq!(state.progress_percent(), 75.0);
669
670 state.mark_block_requested(3);
671 state.mark_block_received(3);
672 assert_eq!(state.progress_percent(), 100.0);
673 }
674
675 #[test]
676 fn test_state_lifecycle_full_cycle() {
677 let mut state = PieceDownloadState::new(10, 262144, 16384);
679
680 assert_eq!(state.piece_index, 10);
682 assert_eq!(state.total_blocks, 16);
683 assert!(!state.is_complete());
684
685 for i in 0..5 {
687 state.mark_block_requested(i);
688 }
689 assert_eq!(state.requested_blocks.len(), 5);
690
691 for i in 0..3 {
693 state.mark_block_received(i);
694 }
695 assert_eq!(state.completed_blocks.len(), 3);
696 assert_eq!(state.requested_blocks.len(), 2); assert_eq!(state.progress_percent(), 18.75); state.mark_block_cancelled(3);
701 state.mark_block_cancelled(4);
702 state.mark_block_cancelled(5);
703 assert_eq!(state.requested_blocks.len(), 0);
704
705 for i in 3..16 {
707 state.mark_block_requested(i);
708 state.mark_block_received(i);
709 }
710
711 assert!(state.is_complete());
712 assert_eq!(state.progress_percent(), 100.0);
713 }
714}