1#![allow(
8 clippy::cast_possible_truncation,
9 clippy::cast_precision_loss,
10 clippy::cast_possible_wrap,
11 clippy::cast_sign_loss,
12 clippy::unchecked_time_subtraction,
13 reason = "M175: piece/peer arithmetic bounded by num_pieces/max_peers (u32); time deltas use post-init Instants; qBt DTOs follow wire-format integer widths"
14)]
15
16use std::collections::{BTreeSet, HashMap, HashSet};
23use std::net::SocketAddr;
24
25use rustc_hash::FxHashMap;
26use std::sync::Arc;
27use std::sync::atomic::AtomicU32;
28use std::time::{Duration, Instant};
29
30use bytes::Bytes;
31use tokio::sync::{broadcast, mpsc, oneshot};
32use tracing::{Instrument, debug, info, trace, warn};
33
34use crate::alert::{Alert, AlertKind, post_alert};
35use crate::disk::{DiskHandle, DiskJobFlags, DiskManagerHandle};
36use crate::piece_reservation::{
37 AtomicPieceStates, BlockMaps, PieceOrderMap, PieceTracker, StealCandidates,
38};
39
40use irontide_core::{
41 DEFAULT_CHUNK_SIZE, FilePriority, Id20, Lengths, Magnet, PeerId, TorrentMetaV1,
42 torrent_from_bytes,
43};
44use irontide_storage::{Bitfield, ChunkTracker, MemoryStorage, TorrentStorage};
49
50use crate::choker::{Choker, PeerInfo as ChokerPeerInfo};
51use crate::end_game::EndGame;
52use crate::metadata::MetadataDownloader;
53use crate::peer_adder::{self, ConnectPeer};
54use crate::peer_state::{PeerSource, PeerState};
55use crate::tracker_manager::TrackerManager;
56use crate::types::{
57 PartialPieceInfo, PeerCommand, PeerEvent, PeerInfo, TorrentCommand, TorrentConfig,
58 TorrentState, TorrentStats,
59};
60
61pub(crate) type SharedBucket = Arc<parking_lot::Mutex<crate::rate_limiter::TokenBucket>>;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub(crate) enum HashResult {
71 Passed,
73 Failed,
75 NotApplicable,
77}
78
79pub(crate) fn relocate_files(
85 src_base: &std::path::Path,
86 dst_base: &std::path::Path,
87 file_paths: &[std::path::PathBuf],
88) -> std::io::Result<()> {
89 for rel_path in file_paths {
90 let src = src_base.join(rel_path);
91 let dst = dst_base.join(rel_path);
92
93 if !src.exists() {
94 continue;
96 }
97
98 if let Some(parent) = dst.parent() {
99 std::fs::create_dir_all(parent)?;
100 }
101
102 if std::fs::rename(&src, &dst).is_err() {
104 std::fs::copy(&src, &dst)?;
106 std::fs::remove_file(&src)?;
107 }
108 }
109
110 for rel_path in file_paths {
113 let mut dir = src_base.join(rel_path);
114 dir.pop(); while dir != *src_base {
116 if std::fs::remove_dir(&dir).is_err() {
117 break; }
119 dir.pop();
120 }
121 }
122
123 Ok(())
124}
125
126pub(crate) fn initial_unchoke_slots(max_uploads_per_torrent: i32) -> usize {
130 if max_uploads_per_torrent >= 1 {
131 max_uploads_per_torrent as usize
132 } else {
133 4
134 }
135}
136
137pub(crate) fn now_unix() -> i64 {
139 std::time::SystemTime::now()
140 .duration_since(std::time::UNIX_EPOCH)
141 .map_or(0, |d| d.as_secs() as i64)
142}
143
144pub(crate) const HOLEPUNCH_COOLDOWN: Duration = Duration::from_mins(2);
146
147pub(crate) const HOLEPUNCH_MAX_TRACKED: usize = 256;
149
150pub(crate) const HOLEPUNCH_RELAY_MAX_PER_WINDOW: u32 = 5;
152
153pub(crate) const HOLEPUNCH_RELAY_WINDOW: Duration = Duration::from_secs(30);
155
156pub(crate) fn should_attempt_holepunch(reason: &str) -> bool {
159 if reason.contains("holepunch") {
161 return false;
162 }
163 reason.contains("refused")
164 || reason.contains("timed out")
165 || reason.contains("Connection reset")
166 || reason.contains("connection reset")
167}
168
169#[derive(Clone)]
171pub struct TorrentHandle {
172 pub cmd_tx: mpsc::Sender<TorrentCommand>,
173}
174
175impl TorrentHandle {
176 #[allow(clippy::too_many_arguments)]
188 pub async fn from_torrent(
189 meta: TorrentMetaV1,
190 version: irontide_core::TorrentVersion,
191 meta_v2: Option<irontide_core::TorrentMetaV2>,
192 disk: DiskHandle,
193 disk_manager: DiskManagerHandle,
194 config: TorrentConfig,
195 dht_rx: irontide_dht::DhtReceiver,
196 dht_v6_rx: irontide_dht::DhtReceiver,
197 global_upload_bucket: Option<SharedBucket>,
198 global_download_bucket: Option<SharedBucket>,
199 slot_tuner: crate::slot_tuner::SlotTuner,
200 alert_tx: broadcast::Sender<Alert>,
201 alert_mask: Arc<AtomicU32>,
202 utp_socket: Option<irontide_utp::UtpSocket>,
203 utp_socket_v6: Option<irontide_utp::UtpSocket>,
204 ban_manager: irontide_session_types::SharedBanManager,
205 ip_filter: irontide_session_types::SharedIpFilter,
206 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
207 sam_session: Option<Arc<crate::i2p::SamSession>>,
208 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
209 factory: Arc<crate::transport::NetworkFactory>,
210 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
211 counters: Arc<crate::stats::SessionCounters>,
212 ) -> crate::Result<Self> {
213 let mut config = config;
214 if meta.info.private == Some(1) {
216 config.enable_dht = false;
217 config.enable_pex = false;
218 config.enable_lsd = false;
219 }
220
221 let info_hashes = match (&version, &meta_v2) {
222 (irontide_core::TorrentVersion::Hybrid, Some(v2_meta)) => {
223 if let Some(v2_hash) = v2_meta.info_hashes.v2 {
224 irontide_core::InfoHashes::hybrid(meta.info_hash, v2_hash)
225 } else {
226 irontide_core::InfoHashes::v1_only(meta.info_hash)
227 }
228 }
229 (irontide_core::TorrentVersion::V2Only, Some(v2_meta)) => v2_meta.info_hashes.clone(),
230 _ => irontide_core::InfoHashes::v1_only(meta.info_hash),
231 };
232
233 if meta.info.piece_length > config.max_piece_length {
234 return Err(crate::Error::InvalidSettings(format!(
235 "piece_length {} exceeds max_piece_length {}",
236 meta.info.piece_length, config.max_piece_length
237 )));
238 }
239
240 let num_pieces = meta.info.num_pieces() as u32;
241 let lengths = Lengths::new(
242 meta.info.total_length(),
243 meta.info.piece_length,
244 DEFAULT_CHUNK_SIZE,
245 );
246 let mut chunk_tracker = ChunkTracker::new(lengths.clone());
247
248 let hash_picker = if version.has_v2() {
250 if let Some(ref v2_meta) = meta_v2 {
251 chunk_tracker.enable_v2_tracking();
252
253 let block_size = 16384u64;
254 let blocks_per_piece = (meta.info.piece_length / block_size) as u32;
255
256 let v2_files = v2_meta.info.files();
258 let file_infos: Vec<irontide_core::FileHashInfo> = v2_files
259 .iter()
260 .filter_map(|f| {
261 let root = f.attr.pieces_root?;
262 let num_blocks = f.attr.length.div_ceil(block_size) as u32;
263 let num_pieces = f.attr.length.div_ceil(meta.info.piece_length) as u32;
264 Some(irontide_core::FileHashInfo {
265 root,
266 num_blocks,
267 num_pieces,
268 })
269 })
270 .collect();
271
272 if file_infos.is_empty() {
273 None
274 } else {
275 let mut picker = irontide_core::HashPicker::new(&file_infos, blocks_per_piece);
276
277 let _verified = picker.load_piece_layers(&v2_meta.piece_layers);
279
280 Some(picker)
281 }
282 } else {
283 None
284 }
285 } else {
286 None
287 };
288
289 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
290 let file_priorities = vec![FilePriority::Normal; file_lengths.len()];
291 let wanted_pieces =
292 crate::piece_selector::build_wanted_pieces(&file_priorities, &file_lengths, &lengths);
293
294 let (cmd_tx, cmd_rx) = mpsc::channel(256);
295 let (event_tx, event_rx) = mpsc::channel(2048);
296 let (write_error_tx, write_error_rx) = mpsc::channel(64);
297 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
298 let (hash_result_tx, hash_result_rx) = mpsc::channel(64); let our_peer_id = if config.anonymous_mode {
300 PeerId::generate_anonymous().0
301 } else {
302 PeerId::generate().0
303 };
304
305 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
308 .bind_tcp(SocketAddr::from((
309 std::net::Ipv6Addr::UNSPECIFIED,
310 config.listen_port,
311 )))
312 .await
313 {
314 Ok(l) => Some(l),
315 Err(_) => factory
316 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
317 .await
318 .ok(),
319 };
320 let mut tracker_manager = TrackerManager::from_torrent_filtered(
323 &meta,
324 our_peer_id,
325 config.listen_port,
326 config.url_security,
327 config.peer_dscp,
328 config.anonymous_mode,
329 );
330 tracker_manager.set_info_hashes(info_hashes.clone());
331
332 if let Some(ref sam) = sam_session {
334 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
335 }
336
337 let enable_dht = config.enable_dht;
338
339 let dht_initial = dht_rx.current();
344 let dht_v6_initial = dht_v6_rx.current();
345
346 let dht_peers_rx = if enable_dht {
348 if let Some(ref dht) = dht_initial {
349 match dht.get_peers(meta.info_hash).await {
350 Ok(rx) => Some(rx),
351 Err(e) => {
352 warn!("failed to start DHT v4 get_peers: {e}");
353 None
354 }
355 }
356 } else {
357 None
358 }
359 } else {
360 None
361 };
362
363 let dht_v6_peers_rx = if enable_dht {
364 if let Some(ref dht6) = dht_v6_initial {
365 match dht6.get_peers(meta.info_hash).await {
366 Ok(rx) => Some(rx),
367 Err(e) => {
368 debug!("failed to start DHT v6 get_peers: {e}");
369 None
370 }
371 }
372 } else {
373 None
374 }
375 } else {
376 None
377 };
378
379 let v2_as_v1 = if info_hashes.is_hybrid() {
381 info_hashes
382 .v2
383 .map(|v2| Id20(v2.0[..20].try_into().unwrap()))
384 } else {
385 None
386 };
387 let (dht_v2_peers_rx, dht_v6_v2_peers_rx) =
388 if let (true, Some(v2_id)) = (enable_dht, v2_as_v1) {
389 let rx4 = if let Some(ref dht) = dht_initial {
390 dht.get_peers(v2_id).await.ok()
391 } else {
392 None
393 };
394 let rx6 = if let Some(ref dht6) = dht_v6_initial {
395 dht6.get_peers(v2_id).await.ok()
396 } else {
397 None
398 };
399 (rx4, rx6)
400 } else {
401 (None, None)
402 };
403
404 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
405 let download_bucket = Arc::new(parking_lot::Mutex::new(
406 crate::rate_limiter::TokenBucket::new(config.download_rate_limit),
407 ));
408 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
409 0,
410 0,
411 0,
412 0,
413 config.upload_rate_limit,
414 config.download_rate_limit,
415 );
416
417 let super_seed = if config.super_seeding {
418 Some(crate::super_seed::SuperSeedState::new())
419 } else {
420 None
421 };
422 let (have_broadcast_tx, _) =
424 tokio::sync::broadcast::channel(std::cmp::max(128, num_pieces as usize / 4));
425 let is_share_mode = config.share_mode;
426
427 let (piece_ready_tx, _) = broadcast::channel(64);
428 let initial_have = chunk_tracker.bitfield().clone();
429 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(initial_have);
430 let stream_read_semaphore =
431 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
432
433 let choker = Choker::with_algorithms(
434 initial_unchoke_slots(config.max_uploads_per_torrent),
435 config.seed_choking_algorithm,
436 config.choking_algorithm,
437 config.upload_rate_limit,
438 2,
439 20,
440 );
441
442 let mut disk = disk;
444 if matches!(version, irontide_core::TorrentVersion::V1Only)
445 && let Some(pool) = &hash_pool
446 {
447 disk.set_hash_pool(pool.clone());
448 disk.set_hash_result_tx(hash_result_tx.clone());
449 }
450
451 let cached_files = Some(build_cached_file_info(&meta, &lengths));
453
454 let (order_map_tx, _order_map_rx_seed) =
456 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
457
458 let actor = TorrentActor {
459 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
460 config.lock_warn_threshold_ms,
461 ),
462 config,
463 info_hash: meta.info_hash,
464 our_peer_id,
465 state: TorrentState::Downloading,
466 disk: Some(disk),
467 disk_manager,
468 chunk_tracker: Some(chunk_tracker),
469 lengths: Some(lengths),
470 num_pieces,
471 streaming_pieces: BTreeSet::new(),
472 time_critical_pieces: BTreeSet::new(),
473 streaming_cursors: Vec::new(),
474 piece_ready_tx,
475 have_watch_tx,
476 have_watch_rx,
477 stream_read_semaphore,
478 file_priorities,
479 wanted_pieces,
480 end_game: EndGame::new(),
481 peers: HashMap::new(),
482 unchoke_durations: HashMap::new(),
483 cached_peer_rates: FxHashMap::default(),
484 refill_notify: Arc::new(tokio::sync::Notify::new()),
485 atomic_states: None,
486 block_maps: None,
487 steal_candidates: None,
488 last_steal_populate: Instant::now(),
489 piece_write_guards: None,
490 soft_reap_buf: Vec::new(),
491 eviction_history: std::collections::VecDeque::new(),
492 force_immediate_choker_tick: false,
493 piece_tracker: None,
494 order_map_dirty: false,
495 next_order_map_gen: 0,
496 order_map_tx,
497 piece_owner: Vec::new(),
498 peer_slab: crate::piece_reservation::PeerSlab::new(),
499 priority_pieces: BTreeSet::new(),
500 max_in_flight: 512,
501 reservation_notify: None,
502 last_tick_dispatch_state: None,
503 choker,
504 user_seed_mode: false,
505 user_forced: false,
506 max_connections: 0,
507 peer_states: None,
508 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
509 connect_permits: HashMap::new(),
510 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
511 std::collections::HashMap::new(),
512 )),
513 connect_rx: None,
514 metadata_downloader: None,
515 downloaded: 0,
516 uploaded: 0,
517 checking_progress: 0.0,
518 total_download: 0,
519 total_upload: 0,
520 total_failed_bytes: 0,
521 total_redundant_bytes: 0,
522 added_time: std::time::SystemTime::now()
523 .duration_since(std::time::UNIX_EPOCH)
524 .map_or(0, |d| d.as_secs() as i64),
525 completed_time: 0,
526 last_download: 0,
527 last_upload: 0,
528 last_seen_complete: 0,
529 active_duration: 0,
530 finished_duration: 0,
531 seeding_duration: 0,
532 active_since: Some(std::time::Instant::now()),
533 state_duration_since: None,
534 started_at: std::time::Instant::now(),
535 moving_storage: false,
536 has_incoming: false,
537 need_save_resume: false,
538 error: String::new(),
539 error_file: -1,
540 cmd_rx,
541 event_tx,
542 event_rx,
543 write_error_rx,
544 write_error_tx,
545 verify_result_rx,
546 verify_result_tx,
547 pending_verify: HashSet::new(),
548 piece_generations: vec![0u64; num_pieces as usize],
549 hash_result_rx,
550 hash_result_tx,
551 meta: Some(meta),
552 cached_files,
553 listener,
554 utp_socket,
555 utp_socket_v6,
556 tracker_manager,
557 tracker_result_rx: None,
558 dht_rx,
559 dht_v6_rx,
560 dht_enabled: enable_dht,
561 dht_peers_rx,
562 dht_v6_peers_rx,
563 dht_v6_empty_count: 0,
564 dht_v6_last_retry: None,
565 alert_tx,
566 alert_mask,
567 upload_bucket,
568 download_bucket,
569 global_upload_bucket,
570 global_download_bucket,
571 slot_tuner,
572 upload_bytes_interval: 0,
573 peak_download_rate: 0,
574 web_seeds: HashMap::new(),
575 banned_web_seeds: HashSet::new(),
576 web_seed_in_flight: HashMap::new(),
577 web_seed_stats: HashMap::new(),
578 pex_peer_count: 0,
579 lsd_peer_count: 0,
580 super_seed,
581 have_broadcast_tx,
582 suggested_to_peers: HashMap::new(),
583 predictive_have_sent: HashSet::new(),
584
585 ban_manager,
586 ip_filter,
587 piece_contributors: HashMap::new(),
588 parole_pieces: HashMap::new(),
589 external_ip: None,
590 share_lru: std::collections::VecDeque::new(),
591 share_max_pieces: if is_share_mode { 64 } else { 0 },
592 plugins,
593 hash_picker,
594 version,
595 meta_v2,
596 info_hashes,
597 dht_v2_peers_rx,
598 dht_v6_v2_peers_rx,
599 magnet_selected_files: None,
600 sam_session,
601 i2p_accept_rx: None,
602 i2p_peer_counter: 0,
603 i2p_destinations: HashMap::new(),
604 ssl_manager,
605 rate_limiter_set,
606 auto_sequential_active: false,
607 factory,
608 hash_pool_ref: hash_pool,
609 connect_attempts: 0,
610 connect_failures: 0,
611 choke_rotations: 0,
612 inflight_started: Vec::new(),
613 completed_piece_times: std::collections::VecDeque::new(),
614 piece_steals: 0,
615 holepunch_relayed: 0,
616 holepunch_relay_rate: HashMap::new(),
617 holepunch_cooldowns: HashMap::new(),
618 holepunch_pending: Vec::new(),
619 counters,
620 };
621
622 let spawn_info_hash = actor.info_hash;
623 let join_handle = tokio::spawn(actor.run());
624 tokio::spawn(async move {
626 match join_handle.await {
627 Ok(()) => {
628 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
629 }
630 Err(e) if e.is_panic() => {
631 let panic_payload = e.into_panic();
632 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
633 (*s).to_string()
634 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
635 s.clone()
636 } else {
637 "unknown panic payload".to_string()
638 };
639 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
640 }
641 Err(e) => {
642 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
643 }
644 }
645 });
646 Ok(Self { cmd_tx })
647 }
648
649 #[allow(clippy::too_many_arguments)]
658 pub async fn from_magnet(
659 magnet: Magnet,
660 disk_manager: DiskManagerHandle,
661 config: TorrentConfig,
662 dht_rx: irontide_dht::DhtReceiver,
663 dht_v6_rx: irontide_dht::DhtReceiver,
664 global_upload_bucket: Option<SharedBucket>,
665 global_download_bucket: Option<SharedBucket>,
666 slot_tuner: crate::slot_tuner::SlotTuner,
667 alert_tx: broadcast::Sender<Alert>,
668 alert_mask: Arc<AtomicU32>,
669 utp_socket: Option<irontide_utp::UtpSocket>,
670 utp_socket_v6: Option<irontide_utp::UtpSocket>,
671 ban_manager: irontide_session_types::SharedBanManager,
672 ip_filter: irontide_session_types::SharedIpFilter,
673 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
674 sam_session: Option<Arc<crate::i2p::SamSession>>,
675 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
676 factory: Arc<crate::transport::NetworkFactory>,
677 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
678 counters: Arc<crate::stats::SessionCounters>,
679 ) -> crate::Result<Self> {
680 let (cmd_tx, cmd_rx) = mpsc::channel(256);
681 let (event_tx, event_rx) = mpsc::channel(2048);
682 let (write_error_tx, write_error_rx) = mpsc::channel(64);
683 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
684 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
686 let our_peer_id = if config.anonymous_mode {
687 PeerId::generate_anonymous().0
688 } else {
689 PeerId::generate().0
690 };
691
692 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
694 .bind_tcp(SocketAddr::from((
695 std::net::Ipv6Addr::UNSPECIFIED,
696 config.listen_port,
697 )))
698 .await
699 {
700 Ok(l) => Some(l),
701 Err(_) => factory
702 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
703 .await
704 .ok(),
705 };
706 let mut tracker_manager = TrackerManager::empty(
709 magnet.info_hash(),
710 our_peer_id,
711 config.listen_port,
712 config.peer_dscp,
713 config.anonymous_mode,
714 );
715 for url in &magnet.trackers {
717 tracker_manager.add_tracker_url(url);
718 }
719
720 if let Some(ref sam) = sam_session {
722 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
723 }
724
725 let enable_dht = config.enable_dht;
726
727 let dht_initial = dht_rx.current();
731 let dht_v6_initial = dht_v6_rx.current();
732
733 let dht_peers_rx = if enable_dht {
735 if let Some(ref dht) = dht_initial {
736 match dht.get_peers(magnet.info_hash()).await {
737 Ok(rx) => Some(rx),
738 Err(e) => {
739 warn!("failed to start DHT v4 get_peers: {e}");
740 None
741 }
742 }
743 } else {
744 None
745 }
746 } else {
747 None
748 };
749
750 let dht_v6_peers_rx = if enable_dht {
751 if let Some(ref dht6) = dht_v6_initial {
752 match dht6.get_peers(magnet.info_hash()).await {
753 Ok(rx) => Some(rx),
754 Err(e) => {
755 debug!("failed to start DHT v6 get_peers: {e}");
756 None
757 }
758 }
759 } else {
760 None
761 }
762 } else {
763 None
764 };
765
766 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
767 let download_bucket = Arc::new(parking_lot::Mutex::new(
768 crate::rate_limiter::TokenBucket::new(config.download_rate_limit),
769 ));
770 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
771 0,
772 0,
773 0,
774 0,
775 config.upload_rate_limit,
776 config.download_rate_limit,
777 );
778
779 let super_seed = if config.super_seeding {
780 Some(crate::super_seed::SuperSeedState::new())
781 } else {
782 None
783 };
784 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
786 let is_share_mode = config.share_mode;
787 let magnet_selected_files = magnet.selected_files.clone();
788 let info_hashes = magnet.info_hashes.clone();
789
790 let (piece_ready_tx, _) = broadcast::channel(64);
791 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(0));
792 let stream_read_semaphore =
793 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
794
795 let choker = Choker::with_algorithms(
796 initial_unchoke_slots(config.max_uploads_per_torrent),
797 config.seed_choking_algorithm,
798 config.choking_algorithm,
799 config.upload_rate_limit,
800 2,
801 20,
802 );
803
804 let (order_map_tx, _order_map_rx_seed) =
805 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
806
807 let actor = TorrentActor {
808 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
809 config.lock_warn_threshold_ms,
810 ),
811 config,
812 info_hash: magnet.info_hash(),
813 our_peer_id,
814 state: TorrentState::FetchingMetadata,
815 disk: None,
816 disk_manager,
817 chunk_tracker: None,
818 lengths: None,
819 num_pieces: 0,
820 streaming_pieces: BTreeSet::new(),
821 time_critical_pieces: BTreeSet::new(),
822 streaming_cursors: Vec::new(),
823 piece_ready_tx,
824 have_watch_tx,
825 have_watch_rx,
826 stream_read_semaphore,
827 file_priorities: Vec::new(),
828 wanted_pieces: Bitfield::new(0),
829 end_game: EndGame::new(),
830 peers: HashMap::new(),
831 unchoke_durations: HashMap::new(),
832 cached_peer_rates: FxHashMap::default(),
833 refill_notify: Arc::new(tokio::sync::Notify::new()),
834 atomic_states: None,
835 block_maps: None,
836 steal_candidates: None,
837 last_steal_populate: Instant::now(),
838 piece_write_guards: None,
839 soft_reap_buf: Vec::new(),
840 eviction_history: std::collections::VecDeque::new(),
841 force_immediate_choker_tick: false,
842 piece_tracker: None,
843 order_map_dirty: false,
844 next_order_map_gen: 0,
845 order_map_tx,
846 piece_owner: Vec::new(),
847 peer_slab: crate::piece_reservation::PeerSlab::new(),
848 priority_pieces: BTreeSet::new(),
849 max_in_flight: 512,
850 reservation_notify: None,
851 last_tick_dispatch_state: None,
852 choker,
853 user_seed_mode: false,
854 user_forced: false,
855 max_connections: 0,
856 peer_states: None,
857 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
858 connect_permits: HashMap::new(),
859 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
860 std::collections::HashMap::new(),
861 )),
862 connect_rx: None,
863 metadata_downloader: Some(MetadataDownloader::new(magnet.info_hash())),
864 downloaded: 0,
865 uploaded: 0,
866 checking_progress: 0.0,
867 total_download: 0,
868 total_upload: 0,
869 total_failed_bytes: 0,
870 total_redundant_bytes: 0,
871 added_time: std::time::SystemTime::now()
872 .duration_since(std::time::UNIX_EPOCH)
873 .map_or(0, |d| d.as_secs() as i64),
874 completed_time: 0,
875 last_download: 0,
876 last_upload: 0,
877 last_seen_complete: 0,
878 active_duration: 0,
879 finished_duration: 0,
880 seeding_duration: 0,
881 active_since: Some(std::time::Instant::now()),
882 state_duration_since: None,
883 started_at: std::time::Instant::now(),
884 moving_storage: false,
885 has_incoming: false,
886 need_save_resume: false,
887 error: String::new(),
888 error_file: -1,
889 cmd_rx,
890 event_tx,
891 event_rx,
892 write_error_rx,
893 write_error_tx,
894 verify_result_rx,
895 verify_result_tx,
896 pending_verify: HashSet::new(),
897 piece_generations: Vec::new(),
898 hash_result_rx,
899 hash_result_tx,
900 meta: None,
901 cached_files: None,
902 listener,
903 utp_socket,
904 utp_socket_v6,
905 tracker_manager,
906 tracker_result_rx: None,
907 dht_rx,
908 dht_v6_rx,
909 dht_enabled: enable_dht,
910 dht_peers_rx,
911 dht_v6_peers_rx,
912 dht_v6_empty_count: 0,
913 dht_v6_last_retry: None,
914 alert_tx,
915 alert_mask,
916 upload_bucket,
917 download_bucket,
918 global_upload_bucket,
919 global_download_bucket,
920 slot_tuner,
921 upload_bytes_interval: 0,
922 peak_download_rate: 0,
923 web_seeds: HashMap::new(),
924 banned_web_seeds: HashSet::new(),
925 web_seed_in_flight: HashMap::new(),
926 web_seed_stats: HashMap::new(),
927 pex_peer_count: 0,
928 lsd_peer_count: 0,
929 super_seed,
930 have_broadcast_tx,
931 suggested_to_peers: HashMap::new(),
932 predictive_have_sent: HashSet::new(),
933
934 ban_manager,
935 ip_filter,
936 piece_contributors: HashMap::new(),
937 parole_pieces: HashMap::new(),
938 external_ip: None,
939 share_lru: std::collections::VecDeque::new(),
940 share_max_pieces: if is_share_mode { 64 } else { 0 },
941 plugins,
942 hash_picker: None,
943 version: irontide_core::TorrentVersion::V1Only,
944 meta_v2: None,
945 info_hashes,
946 dht_v2_peers_rx: None,
947 dht_v6_v2_peers_rx: None,
948 magnet_selected_files,
949 sam_session,
950 i2p_accept_rx: None,
951 i2p_peer_counter: 0,
952 i2p_destinations: HashMap::new(),
953 ssl_manager,
954 rate_limiter_set,
955 auto_sequential_active: false,
956 factory,
957 hash_pool_ref: hash_pool,
958 connect_attempts: 0,
959 connect_failures: 0,
960 choke_rotations: 0,
961 inflight_started: Vec::new(),
962 completed_piece_times: std::collections::VecDeque::new(),
963 piece_steals: 0,
964 holepunch_relayed: 0,
965 holepunch_relay_rate: HashMap::new(),
966 holepunch_cooldowns: HashMap::new(),
967 holepunch_pending: Vec::new(),
968 counters,
969 };
970
971 let spawn_info_hash = actor.info_hash;
972 let join_handle = tokio::spawn(actor.run());
973 tokio::spawn(async move {
974 match join_handle.await {
975 Ok(()) => {
976 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
977 }
978 Err(e) if e.is_panic() => {
979 let panic_payload = e.into_panic();
980 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
981 (*s).to_string()
982 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
983 s.clone()
984 } else {
985 "unknown panic payload".to_string()
986 };
987 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
988 }
989 Err(e) => {
990 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
991 }
992 }
993 });
994 Ok(Self { cmd_tx })
995 }
996
997 pub async fn send_incoming_peer(
1002 &self,
1003 stream: crate::transport::BoxedStream,
1004 addr: SocketAddr,
1005 ) -> crate::Result<()> {
1006 self.cmd_tx
1007 .send(TorrentCommand::IncomingPeer { stream, addr })
1008 .await
1009 .map_err(|_| crate::Error::Shutdown)
1010 }
1011
1012 pub async fn stats(&self) -> crate::Result<TorrentStats> {
1018 let (tx, rx) = oneshot::channel();
1019 self.cmd_tx
1020 .send(TorrentCommand::Stats { reply: tx })
1021 .await
1022 .map_err(|_| crate::Error::Shutdown)?;
1023 rx.await.map_err(|_| crate::Error::Shutdown)
1024 }
1025
1026 pub async fn get_meta(&self) -> crate::Result<Option<TorrentMetaV1>> {
1042 let (tx, rx) = oneshot::channel();
1043 self.cmd_tx
1044 .send(TorrentCommand::GetMeta { reply: tx })
1045 .await
1046 .map_err(|_| crate::Error::Shutdown)?;
1047 rx.await.map_err(|_| crate::Error::Shutdown)
1048 }
1049
1050 pub async fn add_peers(&self, peers: Vec<SocketAddr>, source: PeerSource) -> crate::Result<()> {
1056 self.cmd_tx
1057 .send(TorrentCommand::AddPeers { peers, source })
1058 .await
1059 .map_err(|_| crate::Error::Shutdown)
1060 }
1061
1062 pub async fn pause(&self) -> crate::Result<()> {
1068 self.cmd_tx
1069 .send(TorrentCommand::Pause)
1070 .await
1071 .map_err(|_| crate::Error::Shutdown)
1072 }
1073
1074 pub async fn queue(&self) -> crate::Result<()> {
1080 self.cmd_tx
1081 .send(TorrentCommand::Queue)
1082 .await
1083 .map_err(|_| crate::Error::Shutdown)
1084 }
1085
1086 pub async fn set_category(&self, category: Option<String>) -> crate::Result<()> {
1095 let (tx, rx) = oneshot::channel();
1096 self.cmd_tx
1097 .send(TorrentCommand::SetCategory {
1098 category,
1099 reply: tx,
1100 })
1101 .await
1102 .map_err(|_| crate::Error::Shutdown)?;
1103 rx.await.map_err(|_| crate::Error::Shutdown)
1104 }
1105
1106 pub async fn set_tags(&self, tags: Vec<String>) -> crate::Result<()> {
1117 let (tx, rx) = oneshot::channel();
1118 self.cmd_tx
1119 .send(TorrentCommand::SetTags { tags, reply: tx })
1120 .await
1121 .map_err(|_| crate::Error::Shutdown)?;
1122 rx.await.map_err(|_| crate::Error::Shutdown)
1123 }
1124
1125 pub async fn resume(&self) -> crate::Result<()> {
1131 self.cmd_tx
1132 .send(TorrentCommand::Resume)
1133 .await
1134 .map_err(|_| crate::Error::Shutdown)
1135 }
1136
1137 pub async fn shutdown(&self) -> crate::Result<()> {
1143 let _ = tokio::time::timeout(
1146 std::time::Duration::from_secs(5),
1147 self.cmd_tx.send(TorrentCommand::Shutdown),
1148 )
1149 .await;
1150 Ok(())
1151 }
1152
1153 pub async fn save_resume_data(&self) -> crate::Result<irontide_core::FastResumeData> {
1159 let (tx, rx) = oneshot::channel();
1160 self.cmd_tx
1161 .send(TorrentCommand::SaveResumeData { reply: tx })
1162 .await
1163 .map_err(|_| crate::Error::Shutdown)?;
1164 rx.await.map_err(|_| crate::Error::Shutdown)?
1165 }
1166
1167 pub async fn clear_save_resume_flag(&self) -> crate::Result<()> {
1172 self.cmd_tx
1173 .send(TorrentCommand::ClearSaveResumeFlag)
1174 .await
1175 .map_err(|_| crate::Error::Shutdown)
1176 }
1177
1178 pub async fn take_resume_if_dirty(
1195 &self,
1196 ) -> crate::Result<Option<irontide_core::FastResumeData>> {
1197 let (tx, rx) = oneshot::channel();
1198 self.cmd_tx
1199 .send(TorrentCommand::TakeResumeIfDirty { reply: tx })
1200 .await
1201 .map_err(|_| crate::Error::Shutdown)?;
1202 rx.await.map_err(|_| crate::Error::Shutdown)?
1203 }
1204
1205 pub async fn mark_resume_dirty(&self) -> crate::Result<()> {
1215 self.cmd_tx
1216 .send(TorrentCommand::MarkResumeDirty)
1217 .await
1218 .map_err(|_| crate::Error::Shutdown)
1219 }
1220
1221 pub async fn restore_resume_bitmap(&self, pieces: Vec<u8>) -> crate::Result<()> {
1232 let (tx, rx) = oneshot::channel();
1233 self.cmd_tx
1234 .send(TorrentCommand::RestoreResumeBitmap { pieces, reply: tx })
1235 .await
1236 .map_err(|_| crate::Error::Shutdown)?;
1237 rx.await.map_err(|_| crate::Error::Shutdown)?
1238 }
1239
1240 pub async fn restore_web_seed_stats(
1246 &self,
1247 stats: HashMap<String, irontide_core::WebSeedStats>,
1248 ) -> crate::Result<()> {
1249 let (tx, rx) = oneshot::channel();
1250 self.cmd_tx
1251 .send(TorrentCommand::RestoreWebSeedStats { stats, reply: tx })
1252 .await
1253 .map_err(|_| crate::Error::Shutdown)?;
1254 rx.await.map_err(|_| crate::Error::Shutdown)?
1255 }
1256
1257 pub async fn peer_source_counts(&self) -> crate::Result<(usize, usize)> {
1265 let (tx, rx) = oneshot::channel();
1266 self.cmd_tx
1267 .send(TorrentCommand::GetPeerSourceCounts { reply: tx })
1268 .await
1269 .map_err(|_| crate::Error::Shutdown)?;
1270 rx.await.map_err(|_| crate::Error::Shutdown)
1271 }
1272
1273 pub async fn query_unchoke_durations(
1279 &self,
1280 ) -> crate::Result<HashMap<SocketAddr, std::time::Duration>> {
1281 let (tx, rx) = oneshot::channel();
1282 self.cmd_tx
1283 .send(TorrentCommand::QueryUnchokeDurations { reply: tx })
1284 .await
1285 .map_err(|_| crate::Error::Shutdown)?;
1286 rx.await.map_err(|_| crate::Error::Shutdown)
1287 }
1288
1289 pub async fn get_web_seed_stats(&self) -> crate::Result<Vec<irontide_core::WebSeedStats>> {
1295 let (tx, rx) = oneshot::channel();
1296 self.cmd_tx
1297 .send(TorrentCommand::GetWebSeedStats { reply: tx })
1298 .await
1299 .map_err(|_| crate::Error::Shutdown)?;
1300 rx.await.map_err(|_| crate::Error::Shutdown)
1301 }
1302
1303 pub async fn set_file_priority(
1309 &self,
1310 index: usize,
1311 priority: irontide_core::FilePriority,
1312 ) -> crate::Result<()> {
1313 let (tx, rx) = oneshot::channel();
1314 self.cmd_tx
1315 .send(TorrentCommand::SetFilePriority {
1316 index,
1317 priority,
1318 reply: tx,
1319 })
1320 .await
1321 .map_err(|_| crate::Error::Shutdown)?;
1322 rx.await.map_err(|_| crate::Error::Shutdown)?
1323 }
1324
1325 pub async fn file_priorities(&self) -> crate::Result<Vec<irontide_core::FilePriority>> {
1331 let (tx, rx) = oneshot::channel();
1332 self.cmd_tx
1333 .send(TorrentCommand::FilePriorities { reply: tx })
1334 .await
1335 .map_err(|_| crate::Error::Shutdown)?;
1336 rx.await.map_err(|_| crate::Error::Shutdown)
1337 }
1338
1339 pub async fn tracker_list(&self) -> crate::Result<Vec<crate::tracker_manager::TrackerInfo>> {
1345 let (tx, rx) = oneshot::channel();
1346 self.cmd_tx
1347 .send(TorrentCommand::TrackerList { reply: tx })
1348 .await
1349 .map_err(|_| crate::Error::Shutdown)?;
1350 rx.await.map_err(|_| crate::Error::Shutdown)
1351 }
1352
1353 pub async fn get_web_seeds(&self) -> crate::Result<Vec<String>> {
1359 let (tx, rx) = oneshot::channel();
1360 self.cmd_tx
1361 .send(TorrentCommand::GetWebSeeds { reply: tx })
1362 .await
1363 .map_err(|_| crate::Error::Shutdown)?;
1364 rx.await.map_err(|_| crate::Error::Shutdown)
1365 }
1366
1367 pub async fn get_piece_states(&self) -> crate::Result<Vec<u8>> {
1373 let (tx, rx) = oneshot::channel();
1374 self.cmd_tx
1375 .send(TorrentCommand::GetPieceStates { reply: tx })
1376 .await
1377 .map_err(|_| crate::Error::Shutdown)?;
1378 rx.await.map_err(|_| crate::Error::Shutdown)
1379 }
1380
1381 pub async fn get_piece_hashes(&self, offset: u32, limit: u32) -> crate::Result<Vec<String>> {
1391 let (tx, rx) = oneshot::channel();
1392 self.cmd_tx
1393 .send(TorrentCommand::GetPieceHashes {
1394 offset,
1395 limit,
1396 reply: tx,
1397 })
1398 .await
1399 .map_err(|_| crate::Error::Shutdown)?;
1400 let raw = rx.await.map_err(|_| crate::Error::Shutdown)?;
1404 Ok(raw.iter().map(hex::encode).collect())
1405 }
1406
1407 pub async fn force_reannounce(&self) -> crate::Result<()> {
1413 self.cmd_tx
1414 .send(TorrentCommand::ForceReannounce)
1415 .await
1416 .map_err(|_| crate::Error::Shutdown)
1417 }
1418
1419 pub async fn scrape(&self) -> crate::Result<Option<(String, irontide_tracker::ScrapeInfo)>> {
1425 let (tx, rx) = oneshot::channel();
1426 self.cmd_tx
1427 .send(TorrentCommand::Scrape { reply: tx })
1428 .await
1429 .map_err(|_| crate::Error::Shutdown)?;
1430 rx.await.map_err(|_| crate::Error::Shutdown)
1431 }
1432
1433 pub async fn open_file(
1439 &self,
1440 file_index: usize,
1441 ) -> crate::Result<crate::streaming::FileStream> {
1442 let (tx, rx) = oneshot::channel();
1443 self.cmd_tx
1444 .send(TorrentCommand::OpenFile {
1445 file_index,
1446 reply: tx,
1447 })
1448 .await
1449 .map_err(|_| crate::Error::Shutdown)?;
1450 let handle = rx.await.map_err(|_| crate::Error::Shutdown)??;
1451 Ok(crate::streaming::FileStream::from_handle(handle))
1452 }
1453
1454 pub async fn update_external_ip(&self, ip: std::net::IpAddr) -> crate::Result<()> {
1459 self.cmd_tx
1460 .send(TorrentCommand::UpdateExternalIp { ip })
1461 .await
1462 .map_err(|_| crate::Error::Shutdown)
1463 }
1464
1465 pub async fn move_storage(&self, new_path: std::path::PathBuf) -> crate::Result<()> {
1474 let (tx, rx) = oneshot::channel();
1475 self.cmd_tx
1476 .send(TorrentCommand::MoveStorage {
1477 new_path,
1478 reply: tx,
1479 })
1480 .await
1481 .map_err(|_| crate::Error::Shutdown)?;
1482 rx.await.map_err(|_| crate::Error::Shutdown)?
1483 }
1484
1485 pub async fn set_download_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1491 let (tx, rx) = oneshot::channel();
1492 self.cmd_tx
1493 .send(TorrentCommand::SetDownloadLimit {
1494 bytes_per_sec,
1495 reply: tx,
1496 })
1497 .await
1498 .map_err(|_| crate::Error::Shutdown)?;
1499 rx.await.map_err(|_| crate::Error::Shutdown)
1500 }
1501
1502 pub async fn set_upload_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1508 let (tx, rx) = oneshot::channel();
1509 self.cmd_tx
1510 .send(TorrentCommand::SetUploadLimit {
1511 bytes_per_sec,
1512 reply: tx,
1513 })
1514 .await
1515 .map_err(|_| crate::Error::Shutdown)?;
1516 rx.await.map_err(|_| crate::Error::Shutdown)
1517 }
1518
1519 pub async fn download_limit(&self) -> crate::Result<u64> {
1525 let (tx, rx) = oneshot::channel();
1526 self.cmd_tx
1527 .send(TorrentCommand::DownloadLimit { reply: tx })
1528 .await
1529 .map_err(|_| crate::Error::Shutdown)?;
1530 rx.await.map_err(|_| crate::Error::Shutdown)
1531 }
1532
1533 pub async fn upload_limit(&self) -> crate::Result<u64> {
1539 let (tx, rx) = oneshot::channel();
1540 self.cmd_tx
1541 .send(TorrentCommand::UploadLimit { reply: tx })
1542 .await
1543 .map_err(|_| crate::Error::Shutdown)?;
1544 rx.await.map_err(|_| crate::Error::Shutdown)
1545 }
1546
1547 pub async fn set_sequential_download(&self, enabled: bool) -> crate::Result<()> {
1553 let (tx, rx) = oneshot::channel();
1554 self.cmd_tx
1555 .send(TorrentCommand::SetSequentialDownload { enabled, reply: tx })
1556 .await
1557 .map_err(|_| crate::Error::Shutdown)?;
1558 rx.await.map_err(|_| crate::Error::Shutdown)
1559 }
1560
1561 pub async fn is_sequential_download(&self) -> crate::Result<bool> {
1567 let (tx, rx) = oneshot::channel();
1568 self.cmd_tx
1569 .send(TorrentCommand::IsSequentialDownload { reply: tx })
1570 .await
1571 .map_err(|_| crate::Error::Shutdown)?;
1572 rx.await.map_err(|_| crate::Error::Shutdown)
1573 }
1574
1575 pub async fn set_super_seeding(&self, enabled: bool) -> crate::Result<()> {
1581 let (tx, rx) = oneshot::channel();
1582 self.cmd_tx
1583 .send(TorrentCommand::SetSuperSeeding { enabled, reply: tx })
1584 .await
1585 .map_err(|_| crate::Error::Shutdown)?;
1586 rx.await.map_err(|_| crate::Error::Shutdown)
1587 }
1588
1589 pub async fn is_super_seeding(&self) -> crate::Result<bool> {
1595 let (tx, rx) = oneshot::channel();
1596 self.cmd_tx
1597 .send(TorrentCommand::IsSuperSeeding { reply: tx })
1598 .await
1599 .map_err(|_| crate::Error::Shutdown)?;
1600 rx.await.map_err(|_| crate::Error::Shutdown)
1601 }
1602
1603 pub async fn set_seed_mode(&self, enabled: bool) -> crate::Result<()> {
1614 let (tx, rx) = oneshot::channel();
1615 self.cmd_tx
1616 .send(TorrentCommand::SetSeedMode { enabled, reply: tx })
1617 .await
1618 .map_err(|_| crate::Error::Shutdown)?;
1619 rx.await.map_err(|_| crate::Error::Shutdown)
1620 }
1621
1622 pub async fn add_tracker(&self, url: String) -> crate::Result<()> {
1630 self.cmd_tx
1631 .send(TorrentCommand::AddTracker { url })
1632 .await
1633 .map_err(|_| crate::Error::Shutdown)
1634 }
1635
1636 pub async fn replace_trackers(&self, urls: Vec<String>) -> crate::Result<()> {
1642 let (tx, rx) = oneshot::channel();
1643 self.cmd_tx
1644 .send(TorrentCommand::ReplaceTrackers { urls, reply: tx })
1645 .await
1646 .map_err(|_| crate::Error::Shutdown)?;
1647 rx.await.map_err(|_| crate::Error::Shutdown)
1648 }
1649
1650 pub async fn force_recheck(&self) -> crate::Result<()> {
1661 let (tx, rx) = oneshot::channel();
1662 self.cmd_tx
1663 .send(TorrentCommand::ForceRecheck { reply: tx })
1664 .await
1665 .map_err(|_| crate::Error::Shutdown)?;
1666 rx.await.map_err(|_| crate::Error::Shutdown)?
1667 }
1668
1669 pub async fn rename_file(&self, file_index: usize, new_name: String) -> crate::Result<()> {
1679 let (tx, rx) = oneshot::channel();
1680 self.cmd_tx
1681 .send(TorrentCommand::RenameFile {
1682 file_index,
1683 new_name,
1684 reply: tx,
1685 })
1686 .await
1687 .map_err(|_| crate::Error::Shutdown)?;
1688 rx.await.map_err(|_| crate::Error::Shutdown)?
1689 }
1690
1691 pub async fn spawn_ssl_peer(
1696 &self,
1697 addr: SocketAddr,
1698 stream: impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
1699 ) -> crate::Result<()> {
1700 self.cmd_tx
1701 .send(TorrentCommand::SpawnSslPeer {
1702 addr,
1703 stream: crate::types::BoxedAsyncStream(Box::new(stream)),
1704 })
1705 .await
1706 .map_err(|_| crate::Error::Shutdown)
1707 }
1708
1709 pub async fn set_max_connections(&self, limit: usize) -> crate::Result<()> {
1715 let (tx, rx) = oneshot::channel();
1716 self.cmd_tx
1717 .send(TorrentCommand::SetMaxConnections { limit, reply: tx })
1718 .await
1719 .map_err(|_| crate::Error::Shutdown)?;
1720 rx.await.map_err(|_| crate::Error::Shutdown)
1721 }
1722
1723 pub async fn max_connections(&self) -> crate::Result<usize> {
1729 let (tx, rx) = oneshot::channel();
1730 self.cmd_tx
1731 .send(TorrentCommand::MaxConnections { reply: tx })
1732 .await
1733 .map_err(|_| crate::Error::Shutdown)?;
1734 rx.await.map_err(|_| crate::Error::Shutdown)
1735 }
1736
1737 pub async fn set_max_uploads(&self, limit: usize) -> crate::Result<()> {
1743 let (tx, rx) = oneshot::channel();
1744 self.cmd_tx
1745 .send(TorrentCommand::SetMaxUploads { limit, reply: tx })
1746 .await
1747 .map_err(|_| crate::Error::Shutdown)?;
1748 rx.await.map_err(|_| crate::Error::Shutdown)
1749 }
1750
1751 pub async fn max_uploads(&self) -> crate::Result<usize> {
1757 let (tx, rx) = oneshot::channel();
1758 self.cmd_tx
1759 .send(TorrentCommand::MaxUploads { reply: tx })
1760 .await
1761 .map_err(|_| crate::Error::Shutdown)?;
1762 rx.await.map_err(|_| crate::Error::Shutdown)
1763 }
1764
1765 pub async fn get_peer_info(&self) -> crate::Result<Vec<PeerInfo>> {
1771 let (tx, rx) = oneshot::channel();
1772 self.cmd_tx
1773 .send(TorrentCommand::GetPeerInfo { reply: tx })
1774 .await
1775 .map_err(|_| crate::Error::Shutdown)?;
1776 rx.await.map_err(|_| crate::Error::Shutdown)
1777 }
1778
1779 pub async fn get_download_queue(&self) -> crate::Result<Vec<PartialPieceInfo>> {
1785 let (tx, rx) = oneshot::channel();
1786 self.cmd_tx
1787 .send(TorrentCommand::GetDownloadQueue { reply: tx })
1788 .await
1789 .map_err(|_| crate::Error::Shutdown)?;
1790 rx.await.map_err(|_| crate::Error::Shutdown)
1791 }
1792
1793 pub async fn have_piece(&self, index: u32) -> crate::Result<bool> {
1799 let (tx, rx) = oneshot::channel();
1800 self.cmd_tx
1801 .send(TorrentCommand::HavePiece { index, reply: tx })
1802 .await
1803 .map_err(|_| crate::Error::Shutdown)?;
1804 rx.await.map_err(|_| crate::Error::Shutdown)
1805 }
1806
1807 pub async fn piece_availability(&self) -> crate::Result<Vec<u32>> {
1813 let (tx, rx) = oneshot::channel();
1814 self.cmd_tx
1815 .send(TorrentCommand::PieceAvailability { reply: tx })
1816 .await
1817 .map_err(|_| crate::Error::Shutdown)?;
1818 rx.await.map_err(|_| crate::Error::Shutdown)
1819 }
1820
1821 pub async fn file_progress(&self) -> crate::Result<Vec<u64>> {
1827 let (tx, rx) = oneshot::channel();
1828 self.cmd_tx
1829 .send(TorrentCommand::FileProgress { reply: tx })
1830 .await
1831 .map_err(|_| crate::Error::Shutdown)?;
1832 rx.await.map_err(|_| crate::Error::Shutdown)
1833 }
1834
1835 pub async fn info_hashes(&self) -> crate::Result<irontide_core::InfoHashes> {
1841 let (tx, rx) = oneshot::channel();
1842 self.cmd_tx
1843 .send(TorrentCommand::InfoHashes { reply: tx })
1844 .await
1845 .map_err(|_| crate::Error::Shutdown)?;
1846 rx.await.map_err(|_| crate::Error::Shutdown)
1847 }
1848
1849 pub async fn torrent_file(&self) -> crate::Result<Option<TorrentMetaV1>> {
1857 let (tx, rx) = oneshot::channel();
1858 self.cmd_tx
1859 .send(TorrentCommand::TorrentFile { reply: tx })
1860 .await
1861 .map_err(|_| crate::Error::Shutdown)?;
1862 rx.await.map_err(|_| crate::Error::Shutdown)
1863 }
1864
1865 pub async fn torrent_file_v2(&self) -> crate::Result<Option<irontide_core::TorrentMetaV2>> {
1874 let (tx, rx) = oneshot::channel();
1875 self.cmd_tx
1876 .send(TorrentCommand::TorrentFileV2 { reply: tx })
1877 .await
1878 .map_err(|_| crate::Error::Shutdown)?;
1879 rx.await.map_err(|_| crate::Error::Shutdown)
1880 }
1881
1882 pub async fn force_dht_announce(&self) -> crate::Result<()> {
1890 self.cmd_tx
1891 .send(TorrentCommand::ForceDhtAnnounce)
1892 .await
1893 .map_err(|_| crate::Error::Shutdown)
1894 }
1895
1896 pub async fn read_piece(&self, index: u32) -> crate::Result<Bytes> {
1905 let (tx, rx) = oneshot::channel();
1906 self.cmd_tx
1907 .send(TorrentCommand::ReadPiece { index, reply: tx })
1908 .await
1909 .map_err(|_| crate::Error::Shutdown)?;
1910 rx.await.map_err(|_| crate::Error::Shutdown)?
1911 }
1912
1913 pub async fn flush_cache(&self) -> crate::Result<()> {
1919 let (tx, rx) = oneshot::channel();
1920 self.cmd_tx
1921 .send(TorrentCommand::FlushCache { reply: tx })
1922 .await
1923 .map_err(|_| crate::Error::Shutdown)?;
1924 rx.await.map_err(|_| crate::Error::Shutdown)?
1925 }
1926
1927 #[must_use]
1932 pub fn is_valid(&self) -> bool {
1933 !self.cmd_tx.is_closed()
1934 }
1935
1936 pub async fn clear_error(&self) -> crate::Result<()> {
1942 self.cmd_tx
1943 .send(TorrentCommand::ClearError)
1944 .await
1945 .map_err(|_| crate::Error::Shutdown)
1946 }
1947
1948 pub async fn file_status(&self) -> crate::Result<Vec<crate::types::FileStatus>> {
1956 let (tx, rx) = oneshot::channel();
1957 self.cmd_tx
1958 .send(TorrentCommand::FileStatus { reply: tx })
1959 .await
1960 .map_err(|_| crate::Error::Shutdown)?;
1961 rx.await.map_err(|_| crate::Error::Shutdown)
1962 }
1963
1964 pub async fn flags(&self) -> crate::Result<crate::types::TorrentFlags> {
1970 let (tx, rx) = oneshot::channel();
1971 self.cmd_tx
1972 .send(TorrentCommand::Flags { reply: tx })
1973 .await
1974 .map_err(|_| crate::Error::Shutdown)?;
1975 rx.await.map_err(|_| crate::Error::Shutdown)
1976 }
1977
1978 pub async fn set_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
1986 let (tx, rx) = oneshot::channel();
1987 self.cmd_tx
1988 .send(TorrentCommand::SetFlags { flags, reply: tx })
1989 .await
1990 .map_err(|_| crate::Error::Shutdown)?;
1991 rx.await.map_err(|_| crate::Error::Shutdown)
1992 }
1993
1994 pub async fn unset_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
2002 let (tx, rx) = oneshot::channel();
2003 self.cmd_tx
2004 .send(TorrentCommand::UnsetFlags { flags, reply: tx })
2005 .await
2006 .map_err(|_| crate::Error::Shutdown)?;
2007 rx.await.map_err(|_| crate::Error::Shutdown)
2008 }
2009
2010 pub async fn connect_peer(&self, addr: SocketAddr) -> crate::Result<()> {
2019 self.cmd_tx
2020 .send(TorrentCommand::ConnectPeer { addr })
2021 .await
2022 .map_err(|_| crate::Error::Shutdown)
2023 }
2024
2025 pub fn send_pre_resolved_metadata(&self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
2031 let _ = self
2032 .cmd_tx
2033 .try_send(TorrentCommand::PreResolvedMetadata { info_bytes, peers });
2034 }
2035
2036 #[cfg(feature = "test-util")]
2048 pub(crate) async fn test_inject_metadata(&self, info_bytes: Vec<u8>) -> crate::Result<()> {
2049 let (tx, rx) = tokio::sync::oneshot::channel();
2050 self.cmd_tx
2051 .send(TorrentCommand::TestInjectMetadata {
2052 info_bytes,
2053 reply: tx,
2054 })
2055 .await
2056 .map_err(|_| crate::Error::Shutdown)?;
2057 rx.await.map_err(|_| crate::Error::Shutdown)?;
2058 Ok(())
2059 }
2060}
2061
2062#[derive(Debug, Clone)]
2068pub(crate) struct CachedFileEntry {
2069 pub(crate) index: usize,
2070 #[allow(dead_code)] pub(crate) length: u64,
2072 pub(crate) first_piece: u32,
2073 pub(crate) last_piece: u32,
2074}
2075
2076#[derive(Debug, Clone)]
2078pub(crate) struct CachedFileInfo {
2079 pub(crate) entries: Vec<CachedFileEntry>,
2080}
2081
2082pub(crate) fn build_cached_file_info(meta: &TorrentMetaV1, lengths: &Lengths) -> CachedFileInfo {
2083 let piece_length = lengths.piece_length();
2084 let files = meta.info.files();
2085 let mut entries = Vec::with_capacity(files.len());
2086 let mut offset = 0u64;
2087 for (index, file) in files.iter().enumerate() {
2088 let first_piece = (offset / piece_length) as u32;
2089 let last_piece = if file.length == 0 {
2090 first_piece
2091 } else {
2092 ((offset + file.length - 1) / piece_length) as u32
2093 };
2094 entries.push(CachedFileEntry {
2095 index,
2096 length: file.length,
2097 first_piece,
2098 last_piece,
2099 });
2100 offset += file.length;
2101 }
2102 CachedFileInfo { entries }
2103}
2104
2105pub(crate) struct TorrentActor {
2110 pub(crate) config: TorrentConfig,
2111 pub(crate) lock_timing: crate::timed_lock::LockTimingSettings,
2113 pub(crate) info_hash: Id20,
2114 pub(crate) our_peer_id: Id20,
2115 pub(crate) state: TorrentState,
2116
2117 pub(crate) disk: Option<DiskHandle>,
2119 pub(crate) disk_manager: DiskManagerHandle,
2120 pub(crate) chunk_tracker: Option<ChunkTracker>,
2121 pub(crate) lengths: Option<Lengths>,
2122 pub(crate) num_pieces: u32,
2123
2124 pub(crate) file_priorities: Vec<FilePriority>,
2126 pub(crate) wanted_pieces: Bitfield,
2127 pub(crate) end_game: EndGame,
2128
2129 pub(crate) streaming_pieces: BTreeSet<u32>,
2131 pub(crate) time_critical_pieces: BTreeSet<u32>,
2132 pub(crate) streaming_cursors: Vec<crate::streaming::StreamingCursor>,
2133 pub(crate) piece_ready_tx: broadcast::Sender<u32>,
2134 pub(crate) have_watch_tx: tokio::sync::watch::Sender<Bitfield>,
2135 pub(crate) have_watch_rx: tokio::sync::watch::Receiver<Bitfield>,
2136 pub(crate) stream_read_semaphore: Arc<tokio::sync::Semaphore>,
2137
2138 pub(crate) peers: HashMap<SocketAddr, PeerState>,
2140 pub(crate) unchoke_durations: HashMap<SocketAddr, Duration>,
2149 pub(crate) cached_peer_rates: FxHashMap<SocketAddr, f64>,
2152 #[allow(dead_code)]
2154 pub(crate) refill_notify: Arc<tokio::sync::Notify>,
2155 pub(crate) atomic_states: Option<Arc<crate::piece_reservation::AtomicPieceStates>>,
2157 pub(crate) block_maps: Option<Arc<BlockMaps>>,
2159 pub(crate) steal_candidates: Option<Arc<StealCandidates>>,
2161 pub(crate) last_steal_populate: Instant,
2163 pub(crate) piece_write_guards: Option<Arc<crate::piece_reservation::PieceWriteGuards>>,
2165 pub(crate) soft_reap_buf: Vec<std::net::SocketAddr>,
2169 pub(crate) eviction_history: std::collections::VecDeque<std::time::Instant>,
2174 pub(crate) force_immediate_choker_tick: bool,
2179 pub(crate) piece_tracker: Option<PieceTracker>,
2181 pub(crate) order_map_tx: tokio::sync::watch::Sender<Arc<PieceOrderMap>>,
2183
2184 pub(crate) order_map_dirty: bool,
2189
2190 pub(crate) next_order_map_gen: u64,
2196 pub(crate) piece_owner: Vec<Option<u16>>,
2198 pub(crate) peer_slab: crate::piece_reservation::PeerSlab,
2200 #[allow(dead_code)]
2201 pub(crate) priority_pieces: BTreeSet<u32>,
2202 pub(crate) max_in_flight: usize,
2204 pub(crate) reservation_notify: Option<Arc<tokio::sync::Notify>>,
2206 pub(crate) last_tick_dispatch_state: Option<(u32, usize)>,
2214 pub(crate) choker: Choker,
2215 pub(crate) user_seed_mode: bool,
2222 pub(crate) user_forced: bool,
2224 pub(crate) max_connections: usize,
2226 pub(crate) peer_states: Option<Arc<crate::peer_states::PeerStates>>,
2228 pub(crate) connect_semaphore: Arc<tokio::sync::Semaphore>,
2231 pub(crate) connect_permits:
2234 HashMap<SocketAddr, Arc<parking_lot::Mutex<Option<tokio::sync::OwnedSemaphorePermit>>>>,
2235 pub(crate) connect_rx: Option<mpsc::Receiver<ConnectPeer>>,
2237
2238 pub(crate) metadata_downloader: Option<MetadataDownloader>,
2240
2241 pub(crate) meta: Option<TorrentMetaV1>,
2243
2244 pub(crate) cached_files: Option<CachedFileInfo>,
2246
2247 pub(crate) downloaded: u64,
2249 pub(crate) uploaded: u64,
2250 pub(crate) checking_progress: f32,
2251 pub(crate) total_download: u64,
2252 pub(crate) total_upload: u64,
2253 pub(crate) total_failed_bytes: u64,
2254 pub(crate) total_redundant_bytes: u64,
2255 pub(crate) added_time: i64,
2256 pub(crate) completed_time: i64,
2257 pub(crate) last_download: i64,
2258 pub(crate) last_upload: i64,
2259 pub(crate) last_seen_complete: i64,
2260 pub(crate) active_duration: i64,
2261 pub(crate) finished_duration: i64,
2262 pub(crate) seeding_duration: i64,
2263 pub(crate) active_since: Option<std::time::Instant>,
2264 pub(crate) state_duration_since: Option<std::time::Instant>,
2265 #[allow(dead_code)] pub(crate) started_at: std::time::Instant,
2267 pub(crate) moving_storage: bool,
2268 pub(crate) has_incoming: bool,
2269 pub(crate) need_save_resume: bool,
2270 pub(crate) error: String,
2271 pub(crate) error_file: i32,
2272
2273 pub(crate) cmd_rx: mpsc::Receiver<TorrentCommand>,
2275 pub(crate) event_tx: mpsc::Sender<PeerEvent>,
2276 pub(crate) event_rx: mpsc::Receiver<PeerEvent>,
2277
2278 pub(crate) write_error_rx: mpsc::Receiver<crate::disk::DiskWriteError>,
2280 pub(crate) write_error_tx: mpsc::Sender<crate::disk::DiskWriteError>,
2281 pub(crate) verify_result_rx: mpsc::Receiver<crate::disk::VerifyResult>,
2282 pub(crate) verify_result_tx: mpsc::Sender<crate::disk::VerifyResult>,
2283 pub(crate) pending_verify: HashSet<u32>,
2286 pub(crate) piece_generations: Vec<u64>,
2289 pub(crate) hash_result_rx: tokio::sync::mpsc::Receiver<crate::hash_pool::HashResult>,
2291 pub(crate) hash_result_tx: tokio::sync::mpsc::Sender<crate::hash_pool::HashResult>,
2293
2294 pub(crate) listener: Option<Box<dyn crate::transport::TransportListener>>,
2296
2297 pub(crate) utp_socket: Option<irontide_utp::UtpSocket>,
2299 pub(crate) utp_socket_v6: Option<irontide_utp::UtpSocket>,
2301
2302 pub(crate) tracker_manager: TrackerManager,
2304 pub(crate) tracker_result_rx: Option<mpsc::Receiver<crate::tracker_manager::TrackerPeerBatch>>,
2307
2308 pub(crate) dht_rx: irontide_dht::DhtReceiver,
2315 pub(crate) dht_v6_rx: irontide_dht::DhtReceiver,
2316 pub(crate) dht_enabled: bool,
2317 pub(crate) dht_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2318 pub(crate) dht_v6_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2319 pub(crate) dht_v6_empty_count: u32,
2322 pub(crate) dht_v6_last_retry: Option<std::time::Instant>,
2324
2325 pub(crate) alert_tx: broadcast::Sender<Alert>,
2327 pub(crate) alert_mask: Arc<AtomicU32>,
2328
2329 pub(crate) upload_bucket: crate::rate_limiter::TokenBucket,
2331 pub(crate) download_bucket: SharedBucket,
2332 pub(crate) global_upload_bucket: Option<SharedBucket>,
2333 #[allow(dead_code)] pub(crate) global_download_bucket: Option<SharedBucket>,
2335 pub(crate) slot_tuner: crate::slot_tuner::SlotTuner,
2336 pub(crate) upload_bytes_interval: u64,
2337
2338 pub(crate) peak_download_rate: u64,
2340
2341 pub(crate) web_seeds: HashMap<String, mpsc::Sender<crate::web_seed::WebSeedCommand>>,
2343 pub(crate) banned_web_seeds: HashSet<String>,
2344 pub(crate) web_seed_in_flight: HashMap<u32, String>,
2345 pub(crate) web_seed_stats: HashMap<String, irontide_core::WebSeedStats>,
2349 pub(crate) pex_peer_count: usize,
2355 pub(crate) lsd_peer_count: usize,
2359
2360 pub(crate) super_seed: Option<crate::super_seed::SuperSeedState>,
2362 pub(crate) have_broadcast_tx: tokio::sync::broadcast::Sender<u32>,
2364
2365 pub(crate) suggested_to_peers: HashMap<SocketAddr, HashSet<u32>>,
2367
2368 pub(crate) predictive_have_sent: HashSet<u32>,
2370
2371 pub(crate) ban_manager: irontide_session_types::SharedBanManager,
2373 pub(crate) piece_contributors: HashMap<u32, HashSet<std::net::IpAddr>>,
2374 pub(crate) parole_pieces: HashMap<u32, crate::ban::ParoleState>,
2375
2376 pub(crate) ip_filter: irontide_session_types::SharedIpFilter,
2378
2379 pub(crate) external_ip: Option<std::net::IpAddr>,
2381
2382 pub(crate) share_lru: std::collections::VecDeque<u32>,
2386 pub(crate) share_max_pieces: usize,
2388
2389 pub(crate) plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
2391
2392 pub(crate) hash_picker: Option<irontide_core::HashPicker>,
2394 pub(crate) version: irontide_core::TorrentVersion,
2395 #[allow(dead_code)] pub(crate) meta_v2: Option<irontide_core::TorrentMetaV2>,
2397
2398 pub(crate) info_hashes: irontide_core::InfoHashes,
2400
2401 pub(crate) dht_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2403 pub(crate) dht_v6_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2404
2405 pub(crate) magnet_selected_files: Option<Vec<irontide_core::FileSelection>>,
2408
2409 pub(crate) sam_session: Option<Arc<crate::i2p::SamSession>>,
2411
2412 pub(crate) i2p_accept_rx: Option<mpsc::Receiver<crate::i2p::SamStream>>,
2414
2415 pub(crate) i2p_peer_counter: u32,
2417
2418 pub(crate) i2p_destinations: HashMap<SocketAddr, crate::i2p::I2pDestination>,
2420
2421 pub(crate) ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
2423
2424 pub(crate) rate_limiter_set: crate::rate_limiter::RateLimiterSet,
2426 pub(crate) auto_sequential_active: bool,
2428 pub(crate) factory: Arc<crate::transport::NetworkFactory>,
2430 pub(crate) hash_pool_ref: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
2432 pub(crate) live_outgoing_peers:
2434 std::sync::Arc<parking_lot::RwLock<std::collections::HashMap<SocketAddr, u8>>>,
2435 pub(crate) connect_attempts: u64,
2437 pub(crate) connect_failures: u64,
2439 pub(crate) choke_rotations: u64,
2441 pub(crate) inflight_started: Vec<Option<Instant>>,
2444 pub(crate) completed_piece_times: std::collections::VecDeque<Duration>,
2446 pub(crate) piece_steals: u64,
2448 pub(crate) holepunch_relayed: u64,
2450 pub(crate) holepunch_relay_rate: HashMap<SocketAddr, (Instant, u32)>,
2452 pub(crate) holepunch_cooldowns: HashMap<SocketAddr, Instant>,
2454 pub(crate) holepunch_pending: Vec<SocketAddr>,
2456 pub(crate) counters: Arc<crate::stats::SessionCounters>,
2460}
2461
2462pub(crate) const END_GAME_DEPTH: usize = 128;
2469
2470impl TorrentActor {
2473 pub(crate) fn current_dht(&self) -> Option<irontide_dht::DhtHandle> {
2479 if self.dht_enabled {
2480 self.dht_rx.current()
2481 } else {
2482 None
2483 }
2484 }
2485
2486 pub(crate) fn current_dht_v6(&self) -> Option<irontide_dht::DhtHandle> {
2489 if self.dht_enabled {
2490 self.dht_v6_rx.current()
2491 } else {
2492 None
2493 }
2494 }
2495
2496 #[allow(dead_code)] pub(crate) async fn current_dht_or_wait(
2512 &mut self,
2513 hold: std::time::Duration,
2514 ) -> Option<irontide_dht::DhtHandle> {
2515 if !self.dht_enabled {
2516 return None;
2517 }
2518 if let Some(handle) = self.dht_rx.current() {
2519 return Some(handle);
2520 }
2521 match tokio::time::timeout(hold, self.dht_rx.changed()).await {
2523 Ok(Ok(())) => self.dht_rx.current(),
2524 Ok(Err(_)) | Err(_) => None,
2525 }
2526 }
2527
2528 async fn run(mut self) {
2530 self.verify_existing_pieces().await;
2532
2533 if let Some(ct) = &self.chunk_tracker {
2536 let atomic_states = Arc::new(AtomicPieceStates::new(
2537 self.num_pieces,
2538 ct.bitfield(),
2539 &self.wanted_pieces,
2540 ));
2541 self.atomic_states = Some(Arc::clone(&atomic_states));
2542 self.piece_owner = vec![None; self.num_pieces as usize];
2543 self.inflight_started = vec![None; self.num_pieces as usize];
2545 self.max_in_flight = self.config.max_in_flight_pieces;
2546
2547 if self.config.use_block_stealing {
2549 if let Some(ref lengths) = self.lengths {
2550 self.block_maps = Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
2551 }
2552 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
2553 }
2554 self.piece_write_guards = Some(Arc::new(
2556 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
2557 ));
2558
2559 self.piece_tracker = Some(PieceTracker::new(
2561 self.num_pieces,
2562 ct.bitfield(),
2563 &self.wanted_pieces,
2564 ));
2565 if let Some(ref cached) = self.cached_files {
2566 let file_piece_ranges: Vec<(u32, u32)> = cached
2567 .entries
2568 .iter()
2569 .map(|e| (e.first_piece, e.last_piece))
2570 .collect();
2571 let om = Arc::new(PieceOrderMap::build(
2572 &self.file_priorities,
2573 &file_piece_ranges,
2574 self.num_pieces,
2575 0,
2576 ));
2577 self.order_map_tx.send_replace(om);
2578 }
2579
2580 let notify = Arc::new(tokio::sync::Notify::new());
2581 self.reservation_notify = Some(notify);
2582 }
2583
2584 if self.state != TorrentState::Seeding {
2586 self.spawn_web_seeds();
2587 self.assign_pieces_to_web_seeds();
2588 }
2589
2590 let connect_semaphore = Arc::new(tokio::sync::Semaphore::new(
2593 self.effective_max_connections(),
2594 ));
2595 self.connect_semaphore = Arc::clone(&connect_semaphore);
2596 self.connect_permits.clear();
2597 let (queue_tx, queue_rx) = mpsc::unbounded_channel();
2601 let peer_states = Arc::new(crate::peer_states::PeerStates::new_with_config(
2602 queue_tx,
2603 self.config.eviction_ban_set_cap,
2604 std::time::Duration::from_secs(self.config.eviction_ban_duration_secs),
2605 ));
2606 self.peer_states = Some(Arc::clone(&peer_states));
2607 let (adder_connect_tx, adder_connect_rx) = mpsc::channel(64);
2608 self.connect_rx = Some(adder_connect_rx);
2609 tokio::spawn(peer_adder::peer_adder_task(
2611 queue_rx,
2612 Arc::clone(&connect_semaphore),
2613 Arc::clone(&peer_states),
2614 Arc::clone(&self.ban_manager),
2615 Arc::clone(&self.ip_filter),
2616 adder_connect_tx,
2617 ));
2618
2619 let mut unchoke_interval = tokio::time::interval(Duration::from_secs(10));
2620 let mut rate_interval = tokio::time::interval(Duration::from_secs(2));
2621 rate_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2622 let mut optimistic_interval = tokio::time::interval(Duration::from_secs(30));
2623 let mut refill_interval = tokio::time::interval(Duration::from_millis(100));
2624 let mut dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::ZERO));
2625 let mut suggest_interval = if self.config.suggest_mode {
2626 Some(tokio::time::interval(Duration::from_secs(30)))
2627 } else {
2628 None
2629 };
2630 let mut turnover_interval = tokio::time::interval(Duration::from_secs(1));
2632 let mut pipeline_tick_interval = tokio::time::interval(Duration::from_secs(1));
2633 pipeline_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2635 let mut end_game_tick_interval = tokio::time::interval(Duration::from_millis(200));
2636 end_game_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2637 let mut diag_interval = tokio::time::interval(Duration::from_secs(5));
2638 let mut conn_stats_interval = tokio::time::interval(Duration::from_secs(30));
2640 let mut metadata_timeout_interval = tokio::time::interval(Duration::from_secs(5));
2642 let mut soft_reap_interval = tokio::time::interval(Duration::from_secs(1));
2645 let mut eviction_interval = tokio::time::interval(Duration::from_secs(2));
2648 eviction_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2649
2650 unchoke_interval.tick().await;
2652 optimistic_interval.tick().await;
2653 refill_interval.tick().await;
2654 if let Some(ref mut si) = suggest_interval {
2656 si.tick().await; }
2658 turnover_interval.tick().await;
2659 pipeline_tick_interval.tick().await;
2660 end_game_tick_interval.tick().await;
2661 diag_interval.tick().await;
2662 conn_stats_interval.tick().await;
2663 metadata_timeout_interval.tick().await;
2664 soft_reap_interval.tick().await;
2665 eviction_interval.tick().await;
2666
2667 if self.state == TorrentState::Downloading && self.config.enable_dht {
2670 if let Some(dht) = self.current_dht()
2672 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
2673 {
2674 warn!("DHT v4 announce failed: {e}");
2675 }
2676 if let Some(dht6) = self.current_dht_v6()
2677 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
2678 {
2679 debug!("DHT v6 announce failed: {e}");
2680 }
2681 if self.info_hashes.is_hybrid()
2683 && let Some(v2) = self.info_hashes.v2
2684 {
2685 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
2686 if v2_as_v1 != self.info_hash {
2687 if let Some(dht) = self.current_dht()
2688 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
2689 {
2690 debug!("DHT v4 dual-swarm announce failed: {e}");
2691 }
2692 if let Some(dht6) = self.current_dht_v6()
2693 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
2694 {
2695 debug!("DHT v6 dual-swarm announce failed: {e}");
2696 }
2697 }
2698 }
2699 }
2700
2701 if self.config.enable_i2p
2704 && let Some(ref sam) = self.sam_session
2705 {
2706 let (tx, rx) = mpsc::channel(16);
2707 let sam = Arc::clone(sam);
2708 tokio::spawn(async move {
2709 loop {
2710 match sam.accept().await {
2711 Ok(stream) => {
2712 if tx.send(stream).await.is_err() {
2713 break; }
2715 }
2716 Err(e) => {
2717 warn!("I2P accept error: {e}");
2718 tokio::time::sleep(Duration::from_secs(5)).await;
2719 }
2720 }
2721 }
2722 });
2723 self.i2p_accept_rx = Some(rx);
2724 }
2725
2726 loop {
2727 tokio::select! {
2728 biased;
2729 event = self.event_rx.recv() => {
2734 if let Some(event) = event {
2735 Self::ping_event_drain(&self.peers, &event);
2743 self.handle_peer_event(event)
2744 .instrument(tracing::debug_span!("handle_peer_event"))
2745 .await;
2746 for _ in 0..512 {
2748 match self.event_rx.try_recv() {
2749 Ok(event) => {
2750 Self::ping_event_drain(&self.peers, &event);
2751 self.handle_peer_event(event).await;
2752 }
2753 Err(_) => break,
2754 }
2755 }
2756 }
2757 }
2758 Some(result) = self.verify_result_rx.recv() => {
2760 self.pending_verify.remove(&result.piece);
2761 let dominated = self.chunk_tracker.as_ref()
2763 .is_some_and(|ct| ct.bitfield().get(result.piece));
2764 if !dominated {
2765 if result.passed {
2766 self.on_piece_verified(result.piece).await;
2767 } else {
2768 self.on_piece_hash_failed(result.piece).await;
2769 }
2771 }
2772 }
2773 Some(result) = self.hash_result_rx.recv() => {
2775 self.handle_hash_result(result).await;
2776 }
2777 cmd = self.cmd_rx.recv() => {
2779 match cmd {
2780 Some(TorrentCommand::AddPeers { peers, source }) => {
2781 self.handle_add_peers(peers, source);
2782 }
2783 Some(TorrentCommand::Stats { reply }) => {
2784 let _ = reply.send(self.make_stats());
2785 }
2786 Some(TorrentCommand::Pause) => {
2787 self.handle_pause().await;
2788 }
2789 Some(TorrentCommand::Queue) => {
2790 self.handle_queue();
2791 }
2792 Some(TorrentCommand::Resume) => {
2793 self.handle_resume().await;
2794 }
2795 Some(TorrentCommand::ForceResume) => {
2796 self.user_forced = true;
2797 self.handle_resume().await;
2798 }
2799 Some(TorrentCommand::SetCategory { category, reply }) => {
2800 self.config.category = category;
2804 self.need_save_resume = true;
2805 let _ = reply.send(());
2806 }
2807 Some(TorrentCommand::SetTags { tags, reply }) => {
2808 self.config.tags = tags;
2815 self.need_save_resume = true;
2816 let _ = reply.send(());
2817 }
2818 Some(TorrentCommand::GetWebSeeds { reply }) => {
2819 let urls = match &self.meta {
2825 Some(meta) => {
2826 let mut v = Vec::with_capacity(
2827 meta.url_list.len() + meta.httpseeds.len(),
2828 );
2829 v.extend(meta.url_list.iter().cloned());
2830 v.extend(meta.httpseeds.iter().cloned());
2831 v
2832 }
2833 None => Vec::new(),
2834 };
2835 let _ = reply.send(urls);
2836 }
2837 Some(TorrentCommand::GetPieceStates { reply }) => {
2838 let states = match self.atomic_states.as_ref() {
2842 Some(atomic) => atomic.snapshot(),
2843 None => Vec::new(),
2844 };
2845 let _ = reply.send(states);
2846 }
2847 Some(TorrentCommand::GetPieceHashes { offset, limit, reply }) => {
2848 let offset = offset as usize;
2865 let limit = limit as usize;
2866 let raw: Vec<Vec<u8>> = match self.version {
2867 irontide_core::TorrentVersion::V1Only
2868 | irontide_core::TorrentVersion::Hybrid => self
2869 .meta
2870 .as_ref()
2871 .map(|meta| {
2872 meta.info
2873 .pieces
2874 .chunks_exact(20)
2875 .skip(offset)
2876 .take(limit)
2877 .map(<[u8]>::to_vec)
2878 .collect::<Vec<Vec<u8>>>()
2879 })
2880 .unwrap_or_default(),
2881 irontide_core::TorrentVersion::V2Only => self
2882 .meta_v2
2883 .as_ref()
2884 .map(|m| {
2885 m.piece_layers
2886 .values()
2887 .flat_map(|v| v.chunks_exact(32))
2888 .skip(offset)
2889 .take(limit)
2890 .map(<[u8]>::to_vec)
2891 .collect::<Vec<Vec<u8>>>()
2892 })
2893 .unwrap_or_default(),
2894 };
2895 let _ = reply.send(raw);
2896 }
2897 Some(TorrentCommand::SaveResumeData { reply }) => {
2898 let result = self.build_resume_data();
2899 let _ = reply.send(result);
2900 }
2901 Some(TorrentCommand::TakeResumeIfDirty { reply }) => {
2902 let result = if self.need_save_resume {
2916 let built = self.build_resume_data();
2917 if built.is_ok() {
2918 self.need_save_resume = false;
2919 }
2920 built.map(Some)
2921 } else {
2922 Ok(None)
2923 };
2924 let _ = reply.send(result);
2925 }
2926 Some(TorrentCommand::SetFilePriority { index, priority, reply }) => {
2927 match self.apply_file_priority_scoped(index, priority) {
2937 Ok((first, last)) => {
2938 self.sync_piece_states_for_range(first, last);
2939 if let Some(ref mut pt) = self.piece_tracker {
2940 for piece in first..=last {
2941 if self.wanted_pieces.get(piece) {
2942 pt.mark_wanted(piece);
2943 } else {
2944 pt.mark_unwanted(piece);
2945 }
2946 }
2947 }
2948 self.order_map_dirty = true;
2949 let _ = reply.send(Ok(()));
2950 }
2951 Err(e) => {
2952 let _ = reply.send(Err(e));
2953 }
2954 }
2955 }
2956 Some(TorrentCommand::FilePriorities { reply }) => {
2957 let _ = reply.send(self.file_priorities.clone());
2958 }
2959 Some(TorrentCommand::ForceReannounce) => {
2960 self.tracker_manager.force_reannounce();
2961 }
2962 Some(TorrentCommand::TrackerList { reply }) => {
2963 let _ = reply.send(self.tracker_manager.tracker_list());
2964 }
2965 Some(TorrentCommand::Scrape { reply }) => {
2966 let result = self.tracker_manager.scrape().await;
2967 if let Some((ref url, ref info)) = result {
2968 post_alert(&self.alert_tx, &self.alert_mask, AlertKind::ScrapeReply {
2969 info_hash: self.info_hash,
2970 url: url.clone(),
2971 complete: info.complete,
2972 incomplete: info.incomplete,
2973 downloaded: info.downloaded,
2974 });
2975 }
2976 let _ = reply.send(result);
2977 }
2978 Some(TorrentCommand::OpenFile { file_index, reply }) => {
2979 let result = self.handle_open_file(file_index);
2980 let _ = reply.send(result);
2981 }
2982 Some(TorrentCommand::IncomingPeer { stream, addr }) => {
2983 self.spawn_peer_from_stream_with_mode(
2984 addr,
2985 stream,
2986 Some(irontide_wire::mse::EncryptionMode::Disabled),
2987 );
2988 }
2989 Some(TorrentCommand::UpdateExternalIp { ip }) => {
2990 self.external_ip = Some(ip);
2991 post_alert(
2992 &self.alert_tx,
2993 &self.alert_mask,
2994 AlertKind::ExternalIpDetected { ip },
2995 );
2996 }
2997 Some(TorrentCommand::MoveStorage { new_path, reply }) => {
2998 let result = self.handle_move_storage(new_path).await;
2999 let _ = reply.send(result);
3000 }
3001 Some(TorrentCommand::SpawnSslPeer { addr, stream }) => {
3002 self.spawn_peer_from_stream_with_mode(
3004 addr,
3005 stream.0,
3006 Some(irontide_wire::mse::EncryptionMode::Disabled),
3007 );
3008 }
3009 Some(TorrentCommand::SetDownloadLimit { bytes_per_sec, reply }) => {
3010 self.download_bucket.lock().set_rate(bytes_per_sec);
3011 let _ = reply.send(());
3012 }
3013 Some(TorrentCommand::SetUploadLimit { bytes_per_sec, reply }) => {
3014 self.upload_bucket.set_rate(bytes_per_sec);
3015 let _ = reply.send(());
3016 }
3017 Some(TorrentCommand::DownloadLimit { reply }) => {
3018 let _ = reply.send(self.download_bucket.lock().rate());
3019 }
3020 Some(TorrentCommand::UploadLimit { reply }) => {
3021 let _ = reply.send(self.upload_bucket.rate());
3022 }
3023 Some(TorrentCommand::SetSequentialDownload { enabled, reply }) => {
3024 self.config.sequential_download = enabled;
3025 let _ = reply.send(());
3026 }
3027 Some(TorrentCommand::IsSequentialDownload { reply }) => {
3028 let _ = reply.send(self.config.sequential_download);
3029 }
3030 Some(TorrentCommand::SetSuperSeeding { enabled, reply }) => {
3031 self.config.super_seeding = enabled;
3032 self.super_seed = if enabled {
3033 Some(crate::super_seed::SuperSeedState::new())
3034 } else {
3035 None
3036 };
3037 let _ = reply.send(());
3038 }
3039 Some(TorrentCommand::IsSuperSeeding { reply }) => {
3040 let _ = reply.send(self.config.super_seeding);
3041 }
3042 Some(TorrentCommand::SetSeedMode { enabled, reply }) => {
3043 self.handle_set_seed_mode(enabled);
3044 let _ = reply.send(());
3045 }
3046 Some(TorrentCommand::SetSeedRatioLimit { limit, reply }) => {
3047 self.config.seed_ratio_limit = limit;
3048 self.need_save_resume = true;
3049 let _ = reply.send(());
3050 }
3051 Some(TorrentCommand::AddTracker { url }) => {
3052 self.tracker_manager.add_tracker_url(&url);
3053 }
3054 Some(TorrentCommand::ReplaceTrackers { urls, reply }) => {
3055 self.tracker_manager.replace_all(&urls);
3056 let _ = reply.send(());
3057 }
3058 Some(TorrentCommand::ForceRecheck { reply }) => {
3059 self.handle_force_recheck(reply).await;
3060 }
3061 Some(TorrentCommand::RenameFile { file_index, new_name, reply }) => {
3062 let result = self.handle_rename_file(file_index, new_name).await;
3063 let _ = reply.send(result);
3064 }
3065 Some(TorrentCommand::SetMaxConnections { limit, reply }) => {
3066 self.max_connections = limit;
3067 let _ = reply.send(());
3068 }
3069 Some(TorrentCommand::MaxConnections { reply }) => {
3070 let _ = reply.send(self.max_connections);
3071 }
3072 Some(TorrentCommand::SetMaxUploads { limit, reply }) => {
3073 self.choker.set_unchoke_slots(limit);
3074 let _ = reply.send(());
3075 }
3076 Some(TorrentCommand::MaxUploads { reply }) => {
3077 let _ = reply.send(self.choker.unchoke_slots());
3078 }
3079 Some(TorrentCommand::GetPeerInfo { reply }) => {
3080 let _ = reply.send(self.build_peer_info());
3081 }
3082 Some(TorrentCommand::GetDownloadQueue { reply }) => {
3083 let _ = reply.send(self.build_download_queue());
3084 }
3085 Some(TorrentCommand::HavePiece { index, reply }) => {
3086 let has = self.chunk_tracker.as_ref()
3087 .is_some_and(|ct| ct.has_piece(index));
3088 let _ = reply.send(has);
3089 }
3090 Some(TorrentCommand::PieceAvailability { reply }) => {
3091 let mut avail = vec![0u32; self.num_pieces as usize];
3102 for peer in self.peers.values() {
3103 for i in 0..self.num_pieces {
3104 if peer.bitfield.get(i) {
3105 avail[i as usize] += 1;
3106 }
3107 }
3108 }
3109 let _ = reply.send(avail);
3110 }
3111 Some(TorrentCommand::FileProgress { reply }) => {
3112 let _ = reply.send(self.compute_file_progress());
3113 }
3114 Some(TorrentCommand::InfoHashes { reply }) => {
3115 let _ = reply.send(self.info_hashes.clone());
3116 }
3117 Some(TorrentCommand::TorrentFile { reply }) => {
3118 let _ = reply.send(self.meta.clone());
3119 }
3120 Some(TorrentCommand::TorrentFileV2 { reply }) => {
3121 let _ = reply.send(self.meta_v2.clone());
3122 }
3123 Some(TorrentCommand::ForceDhtAnnounce) => {
3124 self.handle_force_dht_announce().await;
3125 }
3126 Some(TorrentCommand::ReadPiece { index, reply }) => {
3127 let result = self.handle_read_piece(index).await;
3128 let _ = reply.send(result);
3129 }
3130 Some(TorrentCommand::FlushCache { reply }) => {
3131 let result = self.handle_flush_cache().await;
3132 let _ = reply.send(result);
3133 }
3134 Some(TorrentCommand::ClearError) => {
3135 self.handle_clear_error().await;
3136 }
3137 Some(TorrentCommand::ClearSaveResumeFlag) => {
3138 self.need_save_resume = false;
3139 }
3140 Some(TorrentCommand::MarkResumeDirty) => {
3141 self.need_save_resume = true;
3145 }
3146 Some(TorrentCommand::RestoreResumeBitmap { pieces, reply }) => {
3147 let result = self.handle_restore_resume_bitmap(pieces);
3148 let _ = reply.send(result);
3149 }
3150 Some(TorrentCommand::RestoreWebSeedStats { stats, reply }) => {
3151 self.web_seed_stats = stats;
3152 let _ = reply.send(Ok(()));
3153 }
3154 Some(TorrentCommand::GetPeerSourceCounts { reply }) => {
3155 let _ = reply.send((self.pex_peer_count, self.lsd_peer_count));
3156 }
3157 Some(TorrentCommand::QueryUnchokeDurations { reply }) => {
3158 let mut out = self.unchoke_durations.clone();
3159 let now = Instant::now();
3162 for peer in self.peers.values() {
3163 let mut delta = peer.unchoke_duration_total;
3164 if let Some(start) = peer.am_unchoke_started_at {
3165 delta += now.duration_since(start);
3166 }
3167 if !delta.is_zero() {
3168 *out.entry(peer.addr).or_default() += delta;
3169 }
3170 }
3171 let _ = reply.send(out);
3172 }
3173 Some(TorrentCommand::GetWebSeedStats { reply }) => {
3174 let snapshot: Vec<_> = self.web_seed_stats.values().cloned().collect();
3175 let _ = reply.send(snapshot);
3176 }
3177 Some(TorrentCommand::FileStatus { reply }) => {
3178 let _ = reply.send(self.build_file_status());
3179 }
3180 Some(TorrentCommand::Flags { reply }) => {
3181 let _ = reply.send(self.build_flags());
3182 }
3183 Some(TorrentCommand::SetFlags { flags, reply }) => {
3184 self.apply_set_flags(flags).await;
3185 let _ = reply.send(());
3186 }
3187 Some(TorrentCommand::UnsetFlags { flags, reply }) => {
3188 self.apply_unset_flags(flags).await;
3189 let _ = reply.send(());
3190 }
3191 Some(TorrentCommand::ConnectPeer { addr }) => {
3192 self.handle_connect_peer(addr);
3193 }
3194 Some(TorrentCommand::PreResolvedMetadata { info_bytes, peers }) => {
3195 self.handle_pre_resolved_metadata(info_bytes, peers).await;
3196 }
3197 #[cfg(feature = "test-util")]
3198 Some(TorrentCommand::TestInjectMetadata { info_bytes, reply }) => {
3199 self.handle_pre_resolved_metadata(info_bytes, vec![]).await;
3203 let _ = reply.send(());
3204 }
3205 Some(TorrentCommand::GetMeta { reply }) => {
3206 let _ = reply.send(self.meta.clone());
3211 }
3212 Some(TorrentCommand::UpdateSettings(delta)) => {
3213 self.handle_update_settings(&delta);
3214 }
3215 Some(TorrentCommand::Shutdown) => {
3216 info!("torrent actor: received Shutdown command, exiting");
3217 self.shutdown_web_seeds().await;
3218 self.shutdown_peers().await;
3219 return;
3220 }
3221 None => {
3222 warn!("torrent actor: cmd_rx channel closed (all senders dropped), exiting");
3223 self.shutdown_web_seeds().await;
3224 self.shutdown_peers().await;
3225 return;
3226 }
3227 }
3228 }
3229 Some(err) = self.write_error_rx.recv() => {
3231 warn!(piece = err.piece, begin = err.begin, "async disk write failed: {}", err.error);
3232 }
3233 result = accept_incoming(&mut self.listener) => {
3235 if let Ok((stream, addr)) = result {
3236 self.spawn_peer_from_stream(addr, stream);
3237 }
3238 }
3239 stream = accept_i2p(&mut self.i2p_accept_rx) => {
3241 if let Some(stream) = stream {
3242 self.handle_i2p_incoming(stream);
3243 }
3244 }
3245 _ = rate_interval.tick() => {
3248 self.update_peer_rates();
3249 }
3250 _ = unchoke_interval.tick() => {
3252 if self.state == TorrentState::Seeding
3261 || self.state == TorrentState::Sharing
3262 {
3263 self.slot_tuner.observe(self.upload_bytes_interval);
3264 self.choker.observe_throughput(self.upload_bytes_interval);
3265 self.upload_bytes_interval = 0;
3266 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3267 self.run_choker().await;
3268 self.force_immediate_choker_tick = false;
3271 } else {
3272 self.upload_bytes_interval = 0;
3273 }
3274 self.update_streaming_cursors();
3276 if self.config.auto_sequential {
3278 self.auto_sequential_active = crate::piece_selector::evaluate_auto_sequential(
3279 self.piece_owner.iter().filter(|o| o.is_some()).count(),
3280 self.peers.len(),
3281 self.auto_sequential_active,
3282 );
3283 }
3284 self.assign_pieces_to_web_seeds();
3286 }
3287 _ = optimistic_interval.tick() => {
3289 self.rotate_optimistic();
3290 }
3291 Some(connect_peer) = async {
3293 match self.connect_rx.as_mut() {
3294 Some(rx) => rx.recv().await,
3295 None => std::future::pending().await,
3296 }
3297 } => {
3298 self.handle_adder_connect(connect_peer);
3299 }
3300 () = &mut dht_requery_sleep, if self.state != TorrentState::Complete
3301 && self.state != TorrentState::Paused
3302 && self.state != TorrentState::Queued
3303 && self.state != TorrentState::Seeding
3304 && self.state != TorrentState::Stopped => {
3305 self.run_dht_requery().await;
3306 dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::from_mins(1)));
3307 }
3308 () = async {
3313 match self.tracker_manager.next_announce_in() {
3314 Some(dur) => tokio::time::sleep(dur).await,
3315 None => std::future::pending().await,
3316 }
3317 }, if self.tracker_result_rx.is_none() => {
3318 let left = self.calculate_left();
3319 self.tracker_result_rx = Some(self.tracker_manager.start_announce(
3320 irontide_tracker::AnnounceEvent::None,
3321 self.uploaded,
3322 self.downloaded,
3323 left,
3324 ));
3325 }
3326 result = async {
3329 match self.tracker_result_rx.as_mut() {
3330 Some(rx) => rx.recv().await,
3331 None => std::future::pending().await,
3332 }
3333 } => {
3334 match result {
3335 Some(batch) => {
3336 let (peers, outcome) = self.tracker_manager.process_tracker_result(batch);
3337 self.fire_tracker_alerts(&[outcome]);
3338 if !peers.is_empty() {
3339 debug!(count = peers.len(), "tracker returned peers (streaming)");
3340 self.handle_add_peers(peers, PeerSource::Tracker);
3341 }
3342 }
3343 None => {
3344 self.tracker_result_rx = None;
3347 }
3348 }
3349 }
3350 result = async {
3352 match &mut self.dht_peers_rx {
3353 Some(rx) => rx.recv().await,
3354 None => std::future::pending().await,
3355 }
3356 } => {
3357 if let Some(peers) = result {
3358 debug!(count = peers.len(), "DHT v4 returned peers");
3359 self.handle_add_peers(peers, PeerSource::Dht);
3360 } else {
3361 debug!("DHT v4 peer search exhausted");
3362 self.dht_peers_rx = None;
3363 }
3364 }
3365 result = async {
3367 match &mut self.dht_v6_peers_rx {
3368 Some(rx) => rx.recv().await,
3369 None => std::future::pending().await,
3370 }
3371 } => {
3372 if let Some(peers) = result {
3373 debug!(count = peers.len(), "DHT v6 returned peers");
3374 self.dht_v6_empty_count = 0; self.handle_add_peers(peers, PeerSource::Dht);
3376 } else {
3377 self.dht_v6_peers_rx = None;
3378 self.dht_v6_empty_count += 1;
3379 if self.dht_v6_empty_count == 30 {
3380 debug!("DHT v6 routing table persistently empty, giving up");
3381 } else if self.dht_v6_empty_count < 30 {
3382 debug!("DHT v6 peer search exhausted");
3383 }
3384 }
3385 }
3386 result = async {
3388 match &mut self.dht_v2_peers_rx {
3389 Some(rx) => rx.recv().await,
3390 None => std::future::pending().await,
3391 }
3392 } => {
3393 if let Some(peers) = result {
3394 debug!(count = peers.len(), "DHT v4 v2-swarm returned peers");
3395 self.handle_add_peers(peers, PeerSource::Dht);
3396 } else {
3397 debug!("DHT v4 v2-swarm peer search exhausted");
3398 self.dht_v2_peers_rx = None;
3399 }
3400 }
3401 result = async {
3403 match &mut self.dht_v6_v2_peers_rx {
3404 Some(rx) => rx.recv().await,
3405 None => std::future::pending().await,
3406 }
3407 } => {
3408 if let Some(peers) = result {
3409 debug!(count = peers.len(), "DHT v6 v2-swarm returned peers");
3410 self.handle_add_peers(peers, PeerSource::Dht);
3411 } else {
3412 debug!("DHT v6 v2-swarm peer search exhausted");
3413 self.dht_v6_v2_peers_rx = None;
3414 }
3415 }
3416 _ = async {
3418 match suggest_interval {
3419 Some(ref mut interval) => interval.tick().await,
3420 None => std::future::pending().await,
3421 }
3422 } => {
3423 self.suggest_cached_pieces().await;
3424 }
3425 _ = turnover_interval.tick() => {
3426 self.run_steal_queue_maintenance();
3427 }
3428 _ = pipeline_tick_interval.tick() => {
3430 let snub_timeout = Duration::from_secs(u64::from(self.config.snub_timeout_secs));
3431
3432 for (_addr, peer) in &mut self.peers {
3433 peer.pipeline.tick();
3434
3435 if !peer.peer_choking && !peer.snubbed {
3437 let idle = peer.last_data_received
3438 .is_some_and(|t| t.elapsed() > snub_timeout);
3439 if idle {
3440 peer.snubbed = true;
3441 peer.blocks_timed_out = peer.blocks_timed_out
3443 .saturating_add(peer.pending_requests.len() as u64);
3444 debug!(%_addr, "peer snubbed (no data for {}s)", self.config.snub_timeout_secs);
3445 }
3446 }
3447 }
3448
3449 self.refresh_peer_rates();
3452
3453 if !self.end_game.is_active() {
3455 self.check_end_game_activation();
3456 }
3457
3458 self.tick_dispatch_safety_wake();
3459
3460 if self.config.choke_rotation_max_evictions > 0
3462 && self.state == TorrentState::Downloading
3463 {
3464 self.run_choke_rotation();
3465 }
3466
3467 if self.order_map_dirty {
3473 self.rebuild_order_map_now();
3474 }
3475 }
3476 _ = end_game_tick_interval.tick(), if self.end_game.is_active() => {
3481 let addrs: Vec<SocketAddr> = self.peers.iter()
3482 .filter(|(_, p)| !p.peer_choking && p.pending_requests.len() < END_GAME_DEPTH)
3483 .map(|(addr, _)| *addr)
3484 .collect();
3485 for addr in addrs {
3486 self.request_end_game_block(addr).await;
3487 }
3488 }
3489 _ = metadata_timeout_interval.tick(), if self.state == TorrentState::FetchingMetadata => {
3492 let timed_out: Vec<u32> = self
3494 .metadata_downloader
3495 .as_ref()
3496 .map(MetadataDownloader::timed_out_pieces)
3497 .unwrap_or_default();
3498
3499 if !timed_out.is_empty() {
3500 debug!(count = timed_out.len(), "metadata pieces timed out, re-requesting");
3501
3502 let eligible_senders: Vec<mpsc::Sender<PeerCommand>> = self
3505 .peers
3506 .iter()
3507 .filter(|(addr, peer)| {
3508 self.metadata_downloader
3509 .as_ref()
3510 .is_some_and(|dl| !dl.is_rejected(addr))
3511 && peer
3512 .ext_handshake
3513 .as_ref()
3514 .is_some_and(|h| h.metadata_size.is_some())
3515 })
3516 .map(|(_, peer)| peer.cmd_tx.clone())
3517 .collect();
3518
3519 for cmd_tx in &eligible_senders {
3521 for &piece in &timed_out {
3522 let _ = cmd_tx.try_send(PeerCommand::RequestMetadata { piece });
3523 }
3524 }
3525
3526 if let Some(ref mut dl) = self.metadata_downloader {
3528 for piece in timed_out {
3529 dl.reset_request_time(piece);
3530 }
3531 }
3532 }
3533 }
3534 _ = diag_interval.tick() => {
3536 {
3538 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3539 let eg = self.end_game.is_active();
3540 let eg_blocks = self.end_game.block_count();
3541 info!(state = ?self.state, have, total = self.num_pieces, end_game = eg, eg_blocks, "heartbeat");
3542 }
3543 if self.state == TorrentState::Downloading {
3544 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3545 let in_flight = self.atomic_states.as_ref().map_or(0, |s| s.in_flight_count() as usize);
3546 let unchoked = self.peers.values().filter(|p| !p.peer_choking).count();
3547 info!(have, in_flight, total = self.num_pieces,
3548 downloaded_mb = self.downloaded / (1024 * 1024),
3549 peers = self.peers.len(), unchoked,
3550 "download progress");
3551 for (addr, p) in &self.peers {
3552 let last_data = p.last_data_received.map_or(9999, |t| t.elapsed().as_secs());
3553 trace!(%addr,
3554 choking = p.peer_choking,
3555 pending = p.pending_requests.len(),
3556 ewma_rate = p.pipeline.ewma_rate() as u64,
3557 last_data_secs = last_data,
3558 bf_ones = p.bitfield.count_ones(),
3559 "peer state");
3560 }
3561 }
3562 }
3563 _ = conn_stats_interval.tick() => {
3565 if self.connect_attempts > 0 {
3566 let succeeded = self.connect_attempts.saturating_sub(self.connect_failures);
3567 let success_pct = (succeeded as f64 / self.connect_attempts as f64 * 100.0) as u32;
3568 info!(
3569 connected = self.peers.len(),
3570 attempted = self.connect_attempts,
3571 failed = self.connect_failures,
3572 success_rate = %format!("{success_pct}%"),
3573 "connection stats"
3574 );
3575 }
3576 }
3577 _ = soft_reap_interval.tick() => {
3583 let soft_timeout = self.config.connect_soft_timeout;
3584 if soft_timeout > 0 {
3585 if let Some(ref ps) = self.peer_states {
3586 ps.soft_reap_candidates_into(
3587 Duration::from_secs(soft_timeout),
3588 &mut self.soft_reap_buf,
3589 );
3590 } else {
3591 self.soft_reap_buf.clear();
3592 }
3593 for i in 0..self.soft_reap_buf.len() {
3594 let peer_addr = self.soft_reap_buf[i];
3595 debug!(%peer_addr, soft_timeout, "soft reap: no TCP SYN-ACK");
3596 self.connect_permits.remove(&peer_addr);
3598 self.disconnect_peer(peer_addr, "soft reap: no TCP SYN-ACK");
3599 if let Some(ref ps) = self.peer_states
3600 && let Some(backoff) = ps.mark_dead(peer_addr)
3601 {
3602 let ps_clone = Arc::clone(ps);
3603 tokio::spawn(async move {
3604 tokio::time::sleep(backoff).await;
3605 ps_clone.mark_queued_for_retry(peer_addr);
3606 });
3607 }
3608 }
3609 self.soft_reap_buf.clear();
3610 }
3611 }
3612 _ = eviction_interval.tick() => {
3635 if self.force_immediate_choker_tick
3641 && (self.state == TorrentState::Seeding
3642 || self.state == TorrentState::Sharing)
3643 {
3644 self.slot_tuner.observe(self.upload_bytes_interval);
3645 self.choker.observe_throughput(self.upload_bytes_interval);
3646 self.upload_bytes_interval = 0;
3647 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3648 self.run_choker().await;
3649 self.force_immediate_choker_tick = false;
3650 }
3651 if self.state != TorrentState::Seeding {
3652 let prune_cutoff = std::time::Duration::from_mins(1);
3655 while self
3656 .eviction_history
3657 .front()
3658 .copied()
3659 .is_some_and(|t| t.elapsed() > prune_cutoff)
3660 {
3661 self.eviction_history.pop_front();
3662 }
3663 let limit = self.config.proactive_evictions_per_minute_limit as usize;
3664 let window_ok = self.eviction_history.len() < limit;
3665
3666 let should_evict = window_ok
3670 && self.peer_states.as_ref().is_some_and(|ps| {
3671 let live = ps
3672 .stats
3673 .live
3674 .load(std::sync::atomic::Ordering::Relaxed);
3675 #[allow(
3676 clippy::cast_possible_truncation,
3677 clippy::cast_sign_loss
3678 )]
3679 let threshold =
3680 (self.effective_max_connections() as f32 * 0.95) as u32;
3681 debug_assert!(
3682 self.effective_max_connections()
3683 <= crate::torrent_peers::HARD_PEER_CEILING,
3684 "effective_max must be clamped to HARD_PEER_CEILING"
3685 );
3686 live >= threshold
3687 });
3688 if should_evict {
3689 let max_this_tick = 5.min(limit.saturating_sub(self.eviction_history.len()));
3692 for _ in 0..max_this_tick {
3693 match self.find_eviction_candidate() {
3694 Some((victim, pass)) => {
3695 debug!(%victim, ?pass, "v0.187.3 proactive eviction");
3696 self.disconnect_peer(victim, "proactive eviction");
3697 if matches!(pass, crate::torrent_peers::EvictionPass::ZeroThroughput)
3698 && let Some(ref ps) = self.peer_states
3699 {
3700 ps.add_eviction_ban(victim);
3701 }
3702 self.eviction_history.push_back(std::time::Instant::now());
3703 }
3704 None => break,
3705 }
3706 }
3707 }
3708
3709 self.run_piece_steal_scan();
3711 }
3712 }
3713 _ = refill_interval.tick() => {
3715 let elapsed = Duration::from_millis(100);
3716 self.upload_bucket.refill(elapsed);
3717 self.download_bucket.lock().refill(elapsed);
3718 self.rate_limiter_set.refill(elapsed);
3720 let (tcp_peers, utp_peers) = self.transport_peer_counts();
3721 self.rate_limiter_set.apply_mixed_mode(
3722 self.config.mixed_mode_algorithm,
3723 tcp_peers,
3724 utp_peers,
3725 self.config.upload_rate_limit,
3726 );
3727 }
3728 }
3729
3730 for target in std::mem::take(&mut self.holepunch_pending) {
3732 self.try_holepunch(target).await;
3733 }
3734 }
3735 }
3736
3737 pub(crate) fn distributed_copies(&self) -> (u32, u32, f32) {
3743 if self.num_pieces == 0 || self.peers.is_empty() {
3744 return (0, 0, 0.0);
3745 }
3746
3747 let num = self.num_pieces as usize;
3748 let mut availability = vec![0u32; num];
3749
3750 for peer in self.peers.values() {
3751 for idx in 0..self.num_pieces {
3752 if peer.bitfield.get(idx) {
3753 availability[idx as usize] += 1;
3754 }
3755 }
3756 }
3757
3758 let min_avail = availability.iter().copied().min().unwrap_or(0);
3759 let rarest_count = availability.iter().filter(|&&c| c == min_avail).count() as u32;
3760 let fraction = ((self.num_pieces - rarest_count) * 1000) / self.num_pieces;
3761 let copies_float = min_avail as f32 + fraction as f32 / 1000.0;
3762
3763 (min_avail, fraction, copies_float)
3764 }
3765
3766 fn build_download_queue(&self) -> Vec<PartialPieceInfo> {
3770 self.piece_owner
3771 .iter()
3772 .enumerate()
3773 .filter_map(|(piece_index, owner)| {
3774 owner.map(|_| {
3775 let piece_index = piece_index as u32;
3776 let blocks_in_piece = self
3777 .lengths
3778 .as_ref()
3779 .map_or(0, |l| l.piece_size(piece_index).div_ceil(l.chunk_size()));
3780 PartialPieceInfo {
3781 piece_index,
3782 blocks_in_piece,
3783 blocks_assigned: 0,
3784 }
3785 })
3786 })
3787 .collect()
3788 }
3789
3790 fn compute_file_progress(&self) -> Vec<u64> {
3796 let Some(meta) = self.meta.as_ref() else {
3797 return Vec::new();
3798 };
3799 let Some(lengths) = self.lengths.as_ref() else {
3800 return Vec::new();
3801 };
3802 let Some(chunk_tracker) = self.chunk_tracker.as_ref() else {
3803 return Vec::new();
3804 };
3805
3806 let files = meta.info.files();
3807 if files.is_empty() {
3808 return Vec::new();
3809 }
3810
3811 let piece_length = lengths.piece_length();
3812 let mut result = Vec::with_capacity(files.len());
3813 let mut file_offset = 0u64;
3814
3815 for file_entry in &files {
3816 let file_len = file_entry.length;
3817 if file_len == 0 {
3818 result.push(0);
3819 file_offset += file_len;
3820 continue;
3821 }
3822
3823 let file_end = file_offset + file_len;
3824 let first_piece = (file_offset / piece_length) as u32;
3825 let last_piece = ((file_end - 1) / piece_length) as u32;
3826
3827 let mut downloaded = 0u64;
3828
3829 for p in first_piece..=last_piece {
3830 if !chunk_tracker.has_piece(p) {
3831 continue;
3832 }
3833
3834 let piece_start = lengths.piece_offset(p);
3835 let piece_end = piece_start + u64::from(lengths.piece_size(p));
3836
3837 let overlap_start = piece_start.max(file_offset);
3839 let overlap_end = piece_end.min(file_end);
3840
3841 if overlap_start < overlap_end {
3842 downloaded += overlap_end - overlap_start;
3843 }
3844 }
3845
3846 result.push(downloaded);
3847 file_offset = file_end;
3848 }
3849
3850 result
3851 }
3852
3853 fn v6_retry_delay(&self) -> std::time::Duration {
3856 let base_ms: u64 = 100;
3857 let max_ms: u64 = 5000;
3858 let delay_ms = base_ms
3859 .saturating_mul(
3860 1u64.checked_shl(self.dht_v6_empty_count)
3861 .unwrap_or(u64::MAX),
3862 )
3863 .min(max_ms);
3864 std::time::Duration::from_millis(delay_ms)
3865 }
3866
3867 fn should_retry_v6(&self) -> bool {
3869 let Some(last) = self.dht_v6_last_retry else {
3870 return true; };
3872 last.elapsed() >= self.v6_retry_delay()
3873 }
3874
3875 async fn handle_force_dht_announce(&self) {
3877 if let Some(dht) = self.current_dht()
3878 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
3879 {
3880 warn!("Force DHT v4 announce failed: {e}");
3881 }
3882 if let Some(dht6) = self.current_dht_v6()
3883 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
3884 {
3885 debug!("Force DHT v6 announce failed: {e}");
3886 }
3887 if self.info_hashes.is_hybrid()
3889 && let Some(v2) = self.info_hashes.v2
3890 {
3891 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
3892 if v2_as_v1 != self.info_hash {
3893 if let Some(dht) = self.current_dht()
3894 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
3895 {
3896 debug!("Force DHT v4 dual-swarm announce failed: {e}");
3897 }
3898 if let Some(dht6) = self.current_dht_v6()
3899 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
3900 {
3901 debug!("Force DHT v6 dual-swarm announce failed: {e}");
3902 }
3903 }
3904 }
3905 }
3906
3907 async fn run_dht_requery(&mut self) {
3913 if !self.config.enable_dht {
3914 return;
3915 }
3916
3917 if self.peers.len() > self.config.max_peers.saturating_mul(4) {
3921 return;
3922 }
3923
3924 if self.dht_peers_rx.is_none()
3932 && let Some(dht) = self.current_dht()
3933 {
3934 match dht.get_peers(self.info_hash).await {
3935 Ok(rx) => self.dht_peers_rx = Some(rx),
3936 Err(e) => warn!("DHT v4 re-query failed: {e}"),
3937 }
3938 }
3939
3940 if self.dht_v6_peers_rx.is_none()
3942 && self.dht_v6_empty_count < 30
3943 && self.should_retry_v6()
3944 && let Some(dht6) = self.current_dht_v6()
3945 {
3946 self.dht_v6_last_retry = Some(std::time::Instant::now());
3947 match dht6.get_peers(self.info_hash).await {
3948 Ok(rx) => self.dht_v6_peers_rx = Some(rx),
3949 Err(e) => debug!("DHT v6 re-query failed: {e}"),
3950 }
3951 }
3952
3953 if self.info_hashes.is_hybrid()
3955 && let Some(v2) = self.info_hashes.v2
3956 {
3957 let v2_bytes: [u8; 20] = v2.0[..20]
3958 .try_into()
3959 .expect("Id32 is 32 bytes; first 20 always fit");
3960 let v2_as_v1 = Id20(v2_bytes);
3961
3962 if self.dht_v2_peers_rx.is_none()
3963 && let Some(dht) = self.current_dht()
3964 {
3965 match dht.get_peers(v2_as_v1).await {
3966 Ok(rx) => self.dht_v2_peers_rx = Some(rx),
3967 Err(e) => debug!("DHT v4 v2-swarm re-query failed: {e}"),
3968 }
3969 }
3970 if self.dht_v6_v2_peers_rx.is_none()
3971 && self.dht_v6_empty_count < 30
3972 && self.should_retry_v6()
3973 && let Some(dht6) = self.current_dht_v6()
3974 {
3975 self.dht_v6_last_retry = Some(std::time::Instant::now());
3976 match dht6.get_peers(v2_as_v1).await {
3977 Ok(rx) => self.dht_v6_v2_peers_rx = Some(rx),
3978 Err(e) => debug!("DHT v6 v2-swarm re-query failed: {e}"),
3979 }
3980 }
3981 }
3982
3983 debug!(peers = self.peers.len(), "DHT re-query triggered");
3984 }
3985
3986 async fn handle_read_piece(&self, index: u32) -> crate::Result<Bytes> {
3988 let disk = self
3989 .disk
3990 .as_ref()
3991 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3992 let lengths = self
3993 .lengths
3994 .as_ref()
3995 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3996
3997 let piece_size = lengths.piece_size(index);
3998 if piece_size == 0 {
3999 return Err(crate::Error::InvalidPieceIndex {
4000 index,
4001 num_pieces: lengths.num_pieces(),
4002 });
4003 }
4004
4005 let chunk_size = lengths.chunk_size();
4006 let num_chunks = lengths.chunks_in_piece(index);
4007 let mut buf = bytes::BytesMut::with_capacity(piece_size as usize);
4008
4009 for chunk_idx in 0..num_chunks {
4010 let begin = chunk_idx * chunk_size;
4011 let len = if chunk_idx == num_chunks - 1 {
4012 piece_size - begin
4013 } else {
4014 chunk_size
4015 };
4016 let data = disk
4017 .read_chunk(index, begin, len, DiskJobFlags::empty())
4018 .await
4019 .map_err(crate::Error::Storage)?;
4020 buf.extend_from_slice(&data);
4021 }
4022
4023 Ok(buf.freeze())
4024 }
4025
4026 async fn handle_flush_cache(&self) -> crate::Result<()> {
4028 let disk = self
4029 .disk
4030 .as_ref()
4031 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4032 disk.flush_cache().await.map_err(crate::Error::Storage)
4033 }
4034
4035 fn handle_connect_peer(&mut self, addr: SocketAddr) {
4037 if self.peers.contains_key(&addr) {
4039 return;
4040 }
4041 if let Some(ref ps) = self.peer_states {
4043 ps.add_if_not_seen(addr, PeerSource::Incoming);
4044 }
4045 }
4046
4047 pub(crate) fn fire_tracker_alerts(&self, outcomes: &[crate::tracker_manager::TrackerOutcome]) {
4049 for outcome in outcomes {
4050 match &outcome.result {
4051 Ok(num_peers) => {
4052 post_alert(
4053 &self.alert_tx,
4054 &self.alert_mask,
4055 AlertKind::TrackerReply {
4056 info_hash: self.info_hash,
4057 url: outcome.url.clone(),
4058 num_peers: *num_peers,
4059 },
4060 );
4061 }
4062 Err(msg) => {
4063 post_alert(
4064 &self.alert_tx,
4065 &self.alert_mask,
4066 AlertKind::TrackerError {
4067 info_hash: self.info_hash,
4068 url: outcome.url.clone(),
4069 message: msg.clone(),
4070 },
4071 );
4072 }
4073 }
4074 }
4075 }
4076
4077 pub(crate) fn calculate_left(&self) -> u64 {
4079 match (&self.meta, &self.chunk_tracker) {
4080 (Some(meta), Some(ct)) => {
4081 let total = meta.info.total_length();
4082 let have = u64::from(ct.bitfield().count_ones());
4083 let pieces_total = u64::from(self.num_pieces);
4084 let per_piece = total.checked_div(pieces_total).unwrap_or(0);
4085 total.saturating_sub(have * per_piece)
4086 }
4087 _ => 0,
4088 }
4089 }
4090
4091 pub(crate) async fn shutdown_peers(&mut self) {
4092 let left = self.calculate_left();
4094 let _ = tokio::time::timeout(
4095 std::time::Duration::from_secs(3),
4096 self.tracker_manager
4097 .announce_stopped(self.uploaded, self.downloaded, left),
4098 )
4099 .await;
4100
4101 for peer in self.peers.values() {
4103 let _ = peer.cmd_tx.try_send(PeerCommand::Shutdown);
4104 }
4105 }
4106
4107 pub(crate) async fn handle_piece_data(
4110 &mut self,
4111 peer_addr: SocketAddr,
4112 index: u32,
4113 begin: u32,
4114 data: Bytes,
4115 ) {
4116 if let Some(ref ct) = self.chunk_tracker
4120 && ct.has_chunk(index, begin)
4121 {
4122 self.total_download += data.len() as u64 + 13;
4123 if let Some(peer) = self.peers.get_mut(&peer_addr) {
4127 peer.pending_requests.remove(index, begin);
4128 }
4129 if self.end_game.is_active() {
4133 self.end_game.block_received(index, begin, peer_addr);
4134 }
4135 return;
4137 }
4138
4139 let data_len = data.len();
4140
4141 if let Some(ref disk) = self.disk {
4143 disk.write_block_deferred(index, begin, data);
4144 }
4145
4146 self.downloaded += data_len as u64;
4147 self.total_download += data_len as u64 + 13; self.last_download = now_unix();
4149 self.need_save_resume = true;
4150
4151 if let Some(slab_idx) = self.peer_slab.slot_of(&peer_addr)
4153 && self.piece_owner.get(index as usize) == Some(&None)
4154 {
4155 self.piece_owner[index as usize] = Some(slab_idx);
4156 if self.inflight_started.get(index as usize) == Some(&None) {
4158 self.inflight_started[index as usize] = Some(Instant::now());
4159 }
4160 if let (Some(sc), Some(bm)) = (&self.steal_candidates, &self.block_maps)
4162 && let Some(lengths) = &self.lengths
4163 {
4164 let total_blocks = lengths.chunks_in_piece(index);
4165 if bm.next_unrequested(index, total_blocks).is_some() {
4166 sc.push(index);
4167 }
4168 }
4169 }
4170
4171 self.piece_contributors
4173 .entry(index)
4174 .or_default()
4175 .insert(peer_addr.ip());
4176
4177 let now = std::time::Instant::now();
4178 if let Some(peer) = self.peers.get_mut(&peer_addr) {
4179 peer.pending_requests.remove(index, begin);
4180 peer.download_bytes_window += data_len as u64;
4181 peer.download_bytes_total += data_len as u64;
4182 peer.pipeline
4183 .block_received(index, begin, data_len as u32, now);
4184 peer.last_data_received = Some(now);
4185 if peer.snubbed {
4187 peer.snubbed = false;
4188 }
4189 }
4190 if self.end_game.is_active() {
4195 let cancels = self.end_game.block_received(index, begin, peer_addr);
4196 for (cancel_addr, ci, cb, cl) in cancels {
4197 if let Some(cancel_peer) = self.peers.get_mut(&cancel_addr) {
4198 let _ = cancel_peer.cmd_tx.try_send(PeerCommand::Cancel {
4199 index: ci,
4200 begin: cb,
4201 length: cl,
4202 });
4203 cancel_peer.pending_requests.remove(ci, cb);
4204 }
4205 }
4206 }
4207
4208 let piece_complete = if let Some(ref mut ct) = self.chunk_tracker {
4210 ct.chunk_received(index, begin)
4211 } else {
4212 false
4213 };
4214
4215 if piece_complete && !self.pending_verify.contains(&index) {
4216 if self.config.predictive_piece_announce_ms > 0
4218 && !self.predictive_have_sent.contains(&index)
4219 {
4220 self.predictive_have_sent.insert(index);
4221 let _ = self.have_broadcast_tx.send(index);
4222 }
4223
4224 if let Some(ref disk) = self.disk {
4227 disk.flush_piece_writes(index).await;
4228 }
4229
4230 match self.version {
4231 irontide_core::TorrentVersion::V1Only => {
4232 if let Some(ref disk) = self.disk
4234 && let Some(expected) = self
4235 .meta
4236 .as_ref()
4237 .and_then(|m| m.info.piece_hash(index as usize))
4238 {
4239 self.pending_verify.insert(index);
4240 let generation = self
4241 .piece_generations
4242 .get(index as usize)
4243 .copied()
4244 .unwrap_or(0);
4245 disk.enqueue_verify(index, expected, generation, &self.verify_result_tx);
4246 }
4247 }
4248 irontide_core::TorrentVersion::V2Only => {
4249 self.verify_and_mark_piece_v2(index).await;
4251 }
4252 irontide_core::TorrentVersion::Hybrid => {
4253 self.verify_and_mark_piece_hybrid(index).await;
4255 }
4256 }
4257 }
4258
4259 if self.end_game.is_active() {
4262 self.request_end_game_block(peer_addr).await;
4263 }
4264 }
4265
4266 pub(crate) async fn handle_piece_blocks_batch(
4271 &mut self,
4272 peer_addr: SocketAddr,
4273 blocks: Vec<crate::types::BlockEntry>,
4274 ) {
4275 for block in &blocks {
4276 self.process_block_completion(peer_addr, block.index, block.begin, block.length)
4277 .await;
4278 }
4279 }
4280
4281 fn handle_open_file(
4282 &mut self,
4283 file_index: usize,
4284 ) -> crate::Result<crate::streaming::FileStreamHandle> {
4285 let meta = self
4286 .meta
4287 .as_ref()
4288 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4289 let files = meta.info.files();
4290 if file_index >= files.len() {
4291 return Err(crate::Error::InvalidFileIndex {
4292 index: file_index,
4293 count: files.len(),
4294 });
4295 }
4296 if self.file_priorities.get(file_index).copied() == Some(FilePriority::Skip) {
4297 return Err(crate::Error::FileSkipped { index: file_index });
4298 }
4299
4300 let lengths = self
4301 .lengths
4302 .as_ref()
4303 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4304 let disk = self
4305 .disk
4306 .as_ref()
4307 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4308
4309 let mut file_offset = 0u64;
4311 for f in &files[..file_index] {
4312 file_offset += f.length;
4313 }
4314 let file_length = files[file_index].length;
4315
4316 let (cursor_tx, cursor_rx) = tokio::sync::watch::channel(0u64);
4317
4318 let permit = self
4319 .stream_read_semaphore
4320 .clone()
4321 .try_acquire_owned()
4322 .map_err(|_| crate::Error::Connection("too many concurrent stream readers".into()))?;
4323
4324 self.streaming_cursors
4326 .push(crate::streaming::StreamingCursor {
4327 file_index,
4328 file_offset,
4329 cursor_piece: (file_offset / lengths.piece_length()) as u32,
4330 readahead_pieces: self.config.readahead_pieces,
4331 cursor_rx,
4332 });
4333
4334 Ok(crate::streaming::FileStreamHandle {
4335 disk: disk.clone(),
4336 lengths: lengths.clone(),
4337 file_index,
4338 file_offset,
4339 file_length,
4340 cursor_tx,
4341 piece_ready_rx: self.piece_ready_tx.subscribe(),
4342 have: self.have_watch_rx.clone(),
4343 read_permit: permit,
4344 })
4345 }
4346
4347 async fn suggest_cached_pieces(&mut self) {
4349 if !self.config.suggest_mode {
4350 return;
4351 }
4352 let disk = match self.disk {
4353 Some(ref d) => d.clone(),
4354 None => return,
4355 };
4356 let cached = disk.cached_pieces().await;
4357 if cached.is_empty() {
4358 return;
4359 }
4360 let max_suggest = self.config.max_suggest_pieces;
4361 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4362 for peer_addr in peer_addrs {
4363 let already_suggested = self.suggested_to_peers.entry(peer_addr).or_default();
4364 let peer_has_piece = |piece: u32| -> bool {
4365 self.peers
4366 .get(&peer_addr)
4367 .is_some_and(|p| p.bitfield.get(piece))
4368 };
4369 let mut sent = 0;
4370 for &piece in &cached {
4371 if sent >= max_suggest {
4372 break;
4373 }
4374 if peer_has_piece(piece) {
4375 continue;
4376 }
4377 if already_suggested.contains(&piece) {
4378 continue;
4379 }
4380 if let Some(peer) = self.peers.get(&peer_addr) {
4381 let _ = peer.cmd_tx.try_send(PeerCommand::SuggestPiece(piece));
4382 already_suggested.insert(piece);
4383 sent += 1;
4384 }
4385 }
4386 }
4387 }
4388
4389 async fn handle_pre_resolved_metadata(&mut self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
4395 if self.state != TorrentState::FetchingMetadata {
4397 debug!(
4398 info_hash = %self.info_hash,
4399 state = ?self.state,
4400 "ignoring pre-resolved metadata: already past FetchingMetadata"
4401 );
4402 return;
4403 }
4404
4405 debug!(
4406 info_hash = %self.info_hash,
4407 info_bytes_len = info_bytes.len(),
4408 num_peers = peers.len(),
4409 "received pre-resolved metadata from background resolver"
4410 );
4411
4412 if let Some(ref mut dl) = self.metadata_downloader {
4414 dl.set_total_size(info_bytes.len() as u64);
4416
4417 let piece_size: usize = 16384;
4421 let num_pieces = info_bytes.len().div_ceil(piece_size);
4422 for i in 0..num_pieces {
4423 let start = i * piece_size;
4424 let end = (start + piece_size).min(info_bytes.len());
4425 let data = bytes::Bytes::copy_from_slice(&info_bytes[start..end]);
4426 dl.piece_received(i as u32, data);
4427 }
4428 }
4429
4430 self.try_assemble_metadata().await;
4433
4434 if !peers.is_empty() {
4436 self.handle_add_peers(peers, crate::peer_state::PeerSource::Dht);
4437 }
4438 }
4439
4440 pub(crate) async fn try_assemble_metadata(&mut self) {
4441 let assembled = if let Some(ref dl) = self.metadata_downloader {
4442 dl.assemble_and_verify()
4443 } else {
4444 return;
4445 };
4446
4447 match assembled {
4448 Ok(info_bytes) => {
4449 let mut torrent_bytes = b"d4:info".to_vec();
4455 torrent_bytes.extend_from_slice(&info_bytes);
4456 torrent_bytes.push(b'e');
4457
4458 match torrent_from_bytes(&torrent_bytes) {
4459 Ok(meta) => {
4460 let num_pieces = meta.info.num_pieces() as u32;
4461 let lengths = Lengths::new(
4462 meta.info.total_length(),
4463 meta.info.piece_length,
4464 DEFAULT_CHUNK_SIZE,
4465 );
4466
4467 let files = meta.info.files();
4469 let file_paths: Vec<std::path::PathBuf> = files
4470 .iter()
4471 .map(|f| f.path.iter().collect::<std::path::PathBuf>())
4472 .collect();
4473 let file_lengths_vec: Vec<u64> = files.iter().map(|f| f.length).collect();
4474 let prealloc_mode = self.config.preallocate_mode.unwrap_or_else(|| {
4475 irontide_storage::PreallocateMode::from(
4476 self.config.storage_mode == irontide_core::StorageMode::Full,
4477 )
4478 });
4479 let storage: Arc<dyn TorrentStorage> =
4480 match irontide_storage::FilesystemStorage::new(
4481 &self.config.download_dir,
4482 file_paths,
4483 file_lengths_vec,
4484 lengths.clone(),
4485 None,
4486 prealloc_mode,
4487 self.config.filesystem_direct_io,
4488 ) {
4489 Ok(s) => Arc::new(s),
4490 Err(e) => {
4491 warn!(
4492 "failed to create filesystem storage: {e}, falling back to memory"
4493 );
4494 Arc::new(MemoryStorage::new(lengths.clone()))
4495 }
4496 };
4497 let mut disk_handle = self
4498 .disk_manager
4499 .register_torrent(self.info_hash, storage)
4500 .await;
4501
4502 self.chunk_tracker = Some(ChunkTracker::new(lengths.clone()));
4503 self.lengths = Some(lengths);
4504 self.num_pieces = num_pieces;
4505 self.piece_generations = vec![0u64; num_pieces as usize];
4507 let (hash_tx, hash_rx) = tokio::sync::mpsc::channel(64);
4508 self.hash_result_tx = hash_tx;
4509 self.hash_result_rx = hash_rx;
4510 if let Some(ref pool) = self.hash_pool_ref {
4513 disk_handle.set_hash_pool(pool.clone());
4514 disk_handle.set_hash_result_tx(self.hash_result_tx.clone());
4515 }
4516 self.disk = Some(disk_handle);
4517 for peer in self.peers.values() {
4520 let _ = peer
4521 .cmd_tx
4522 .try_send(PeerCommand::UpdateNumPieces(num_pieces));
4523 }
4524 let file_lengths: Vec<u64> =
4525 meta.info.files().iter().map(|f| f.length).collect();
4526 let mut meta = meta;
4527 meta.info_bytes = Some(Bytes::from(info_bytes));
4528 self.meta = Some(meta);
4529
4530 if let (Some(meta), Some(lengths)) = (&self.meta, &self.lengths) {
4532 self.cached_files = Some(build_cached_file_info(meta, lengths));
4533 }
4534
4535 self.file_priorities = vec![FilePriority::Normal; file_lengths.len()];
4536
4537 if let Some(ref selections) = self.magnet_selected_files {
4539 self.file_priorities = irontide_core::FileSelection::to_priorities(
4540 selections,
4541 file_lengths.len(),
4542 );
4543 self.magnet_selected_files = None;
4544 }
4545
4546 self.wanted_pieces = crate::piece_selector::build_wanted_pieces(
4547 &self.file_priorities,
4548 &file_lengths,
4549 self.lengths.as_ref().unwrap(),
4550 );
4551 if self.config.share_mode {
4552 self.transition_state(TorrentState::Sharing);
4553 } else {
4554 self.transition_state(TorrentState::Downloading);
4555 }
4556 self.metadata_downloader = None;
4557
4558 if let Some(ref meta) = self.meta {
4560 self.tracker_manager
4561 .set_metadata_filtered(meta, self.config.url_security);
4562 }
4563
4564 if let Ok(detected) = irontide_core::torrent_from_bytes_any(&torrent_bytes)
4567 {
4568 let new_version = detected.version();
4569 if new_version != irontide_core::TorrentVersion::V1Only {
4570 let new_hashes = detected.info_hashes();
4571 self.version = new_version;
4572 self.info_hashes = new_hashes.clone();
4573 self.tracker_manager.set_info_hashes(new_hashes.clone());
4574 if let Some(v2_meta) = detected.as_v2() {
4575 self.meta_v2 = Some(v2_meta.clone());
4576 }
4577 if new_hashes.is_hybrid()
4579 && let Some(v2) = new_hashes.v2
4580 {
4581 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
4582 if v2_as_v1 != self.info_hash {
4583 if self.dht_v2_peers_rx.is_none()
4584 && let Some(dht) = self.current_dht()
4585 && let Ok(rx) = dht.get_peers(v2_as_v1).await
4586 {
4587 self.dht_v2_peers_rx = Some(rx);
4588 }
4589 if self.dht_v6_v2_peers_rx.is_none()
4590 && self.dht_v6_empty_count < 30
4591 && self.should_retry_v6()
4592 && let Some(dht6) = self.current_dht_v6()
4593 && let Ok(rx) = dht6.get_peers(v2_as_v1).await
4594 {
4595 self.dht_v6_last_retry =
4596 Some(std::time::Instant::now());
4597 self.dht_v6_v2_peers_rx = Some(rx);
4598 }
4599 }
4600 }
4601 }
4602 }
4603
4604 let name = self
4605 .meta
4606 .as_ref()
4607 .map(|m| m.info.name.clone())
4608 .unwrap_or_default();
4609 post_alert(
4610 &self.alert_tx,
4611 &self.alert_mask,
4612 AlertKind::MetadataReceived {
4613 info_hash: self.info_hash,
4614 name,
4615 },
4616 );
4617 info!("metadata assembled, switching to Downloading");
4618
4619 if let Some(ct) = &self.chunk_tracker {
4621 let atomic_states = Arc::new(AtomicPieceStates::new(
4622 self.num_pieces,
4623 ct.bitfield(),
4624 &self.wanted_pieces,
4625 ));
4626 self.atomic_states = Some(Arc::clone(&atomic_states));
4627 self.piece_owner = vec![None; self.num_pieces as usize];
4628 self.inflight_started = vec![None; self.num_pieces as usize];
4630 self.max_in_flight = self.config.max_in_flight_pieces;
4631
4632 if self.config.use_block_stealing {
4634 if let Some(ref lengths) = self.lengths {
4635 self.block_maps =
4636 Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
4637 }
4638 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
4639 }
4640 self.piece_write_guards = Some(Arc::new(
4642 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
4643 ));
4644
4645 self.piece_tracker = Some(PieceTracker::new(
4647 self.num_pieces,
4648 ct.bitfield(),
4649 &self.wanted_pieces,
4650 ));
4651 if let Some(ref cached) = self.cached_files {
4652 let file_piece_ranges: Vec<(u32, u32)> = cached
4653 .entries
4654 .iter()
4655 .map(|e| (e.first_piece, e.last_piece))
4656 .collect();
4657 let om = Arc::new(PieceOrderMap::build(
4658 &self.file_priorities,
4659 &file_piece_ranges,
4660 self.num_pieces,
4661 0,
4662 ));
4663 self.order_map_tx.send_replace(om);
4664 }
4665
4666 let notify = Arc::new(tokio::sync::Notify::new());
4667 self.reservation_notify = Some(notify);
4668 }
4669
4670 self.spawn_web_seeds();
4672 self.assign_pieces_to_web_seeds();
4673
4674 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4677 info!(
4678 connected_peers = peer_addrs.len(),
4679 "kick-starting piece requests for pre-connected peers"
4680 );
4681 for addr in peer_addrs {
4682 let has_bitfield =
4683 self.peers.get(&addr).map_or(0, |p| p.bitfield.count_ones());
4684 let is_choking = self.peers.get(&addr).is_none_or(|p| p.peer_choking);
4685 debug!(%addr, has_bitfield, is_choking, "post-metadata peer state");
4686 self.maybe_express_interest(addr).await;
4687 if let Some(peer) = self.peers.get(&addr)
4688 && peer.bitfield.count_ones() > 0
4689 {
4690 let _slot = self.peer_slab.insert(addr);
4691 }
4692 }
4693 self.recalc_max_in_flight();
4694 if !self.user_seed_mode
4698 && let Some(notify) = &self.reservation_notify
4699 && let Some(ref lengths) = self.lengths
4700 {
4701 for peer in self.peers.values() {
4702 let _ = peer.cmd_tx.try_send(PeerCommand::StartRequesting {
4703 piece_notify: Arc::clone(notify),
4704 disk_handle: self.disk.clone(),
4705 write_error_tx: self.write_error_tx.clone(),
4706 lengths: lengths.clone(),
4707 });
4708 }
4709 }
4710 }
4711 Err(e) => {
4712 warn!("failed to parse assembled metadata: {e}");
4713 post_alert(
4714 &self.alert_tx,
4715 &self.alert_mask,
4716 AlertKind::MetadataFailed {
4717 info_hash: self.info_hash,
4718 },
4719 );
4720 }
4721 }
4722 }
4723 Err(e) => {
4724 warn!("metadata assembly failed: {e}");
4725 post_alert(
4726 &self.alert_tx,
4727 &self.alert_mask,
4728 AlertKind::MetadataFailed {
4729 info_hash: self.info_hash,
4730 },
4731 );
4732 }
4733 }
4734 }
4735
4736 fn spawn_web_seeds(&mut self) {
4739 if !self.config.enable_web_seed {
4740 return;
4741 }
4742 let Some(meta) = &self.meta else { return };
4743 let lengths = match &self.lengths {
4744 Some(l) => l.clone(),
4745 None => return,
4746 };
4747
4748 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
4749 let file_map = irontide_storage::FileMap::new(file_lengths, lengths.clone());
4750
4751 for url in &meta.url_list {
4753 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4754 continue;
4755 }
4756 if self.web_seeds.len() >= self.config.max_web_seeds {
4757 break;
4758 }
4759
4760 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4762 warn!(%url, %e, "web seed URL rejected by security policy");
4763 continue;
4764 }
4765
4766 let url_builder = if meta.info.length.is_some() {
4767 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone())
4768 } else {
4769 let file_paths: Vec<String> = meta
4770 .info
4771 .files()
4772 .iter()
4773 .map(|f| f.path[1..].join("/")) .collect();
4775 crate::web_seed::WebSeedUrlBuilder::multi(
4776 url.clone(),
4777 meta.info.name.clone(),
4778 file_paths,
4779 )
4780 };
4781
4782 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4783 let initial_downloaded = self
4784 .web_seed_stats
4785 .get(url)
4786 .map_or(0, |s| s.downloaded_bytes);
4787 let task = crate::web_seed::WebSeedTask::new(
4788 url.clone(),
4789 crate::web_seed::WebSeedMode::GetRight,
4790 url_builder,
4791 lengths.clone(),
4792 file_map.clone(),
4793 self.info_hash,
4794 cmd_rx,
4795 self.event_tx.clone(),
4796 self.config.url_security,
4797 self.config.web_seed_progress_throttle_ms,
4798 initial_downloaded,
4799 self.config.web_seed_retry_base_secs,
4800 self.config.web_seed_retry_factor,
4801 self.config.web_seed_retry_cap_secs,
4802 self.config.web_seed_max_failures,
4803 );
4804 tokio::spawn(task.run());
4805 self.web_seeds.insert(url.clone(), cmd_tx);
4806 debug!(url, "spawned BEP 19 web seed");
4807 }
4808
4809 for url in &meta.httpseeds {
4811 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4812 continue;
4813 }
4814 if self.web_seeds.len() >= self.config.max_web_seeds {
4815 break;
4816 }
4817
4818 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4820 warn!(%url, %e, "web seed URL rejected by security policy");
4821 continue;
4822 }
4823
4824 let url_builder =
4826 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone());
4827
4828 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4829 let initial_downloaded = self
4830 .web_seed_stats
4831 .get(url)
4832 .map_or(0, |s| s.downloaded_bytes);
4833 let task = crate::web_seed::WebSeedTask::new(
4834 url.clone(),
4835 crate::web_seed::WebSeedMode::Hoffman,
4836 url_builder,
4837 lengths.clone(),
4838 file_map.clone(),
4839 self.info_hash,
4840 cmd_rx,
4841 self.event_tx.clone(),
4842 self.config.url_security,
4843 self.config.web_seed_progress_throttle_ms,
4844 initial_downloaded,
4845 self.config.web_seed_retry_base_secs,
4846 self.config.web_seed_retry_factor,
4847 self.config.web_seed_retry_cap_secs,
4848 self.config.web_seed_max_failures,
4849 );
4850 tokio::spawn(task.run());
4851 self.web_seeds.insert(url.clone(), cmd_tx);
4852 debug!(url, "spawned BEP 17 web seed");
4853 }
4854 }
4855
4856 pub(crate) fn assign_pieces_to_web_seeds(&mut self) {
4857 if self.state != TorrentState::Downloading || self.end_game.is_active() {
4858 return;
4859 }
4860
4861 let active_urls: HashSet<&String> = self.web_seed_in_flight.values().collect();
4863 let idle_urls: Vec<String> = self
4864 .web_seeds
4865 .keys()
4866 .filter(|u| !active_urls.contains(u))
4867 .cloned()
4868 .collect();
4869
4870 let Some(ct) = &self.chunk_tracker else {
4871 return;
4872 };
4873
4874 for url in idle_urls {
4875 let piece = (0..self.num_pieces).find(|&i| {
4878 !ct.has_piece(i)
4879 && !self
4880 .piece_owner
4881 .get(i as usize)
4882 .is_some_and(std::option::Option::is_some)
4883 && !self.web_seed_in_flight.contains_key(&i)
4884 && self.wanted_pieces.get(i)
4885 });
4886
4887 if let Some(piece) = piece
4888 && let Some(cmd_tx) = self.web_seeds.get(&url)
4889 {
4890 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::FetchPiece(piece));
4891 self.web_seed_in_flight.insert(piece, url);
4892 }
4893 }
4894 }
4895
4896 pub(crate) async fn handle_web_seed_piece_data(
4897 &mut self,
4898 url: String,
4899 index: u32,
4900 data: Bytes,
4901 ) {
4902 self.web_seed_in_flight.remove(&index);
4903
4904 if let Some(ref ct) = self.chunk_tracker
4906 && ct.has_piece(index)
4907 {
4908 self.assign_pieces_to_web_seeds();
4909 return;
4910 }
4911
4912 if let Some(ref disk) = self.disk
4914 && let Err(e) = disk
4915 .write_chunk(index, 0, data.clone(), DiskJobFlags::FLUSH_PIECE)
4916 .await
4917 {
4918 warn!(index, "web seed: failed to write piece: {e}");
4919 self.assign_pieces_to_web_seeds();
4920 return;
4921 }
4922
4923 if let Some(ref mut ct) = self.chunk_tracker
4925 && let Some(ref lengths) = self.lengths
4926 {
4927 let num_chunks = lengths.chunks_in_piece(index);
4928 for chunk_idx in 0..num_chunks {
4929 if let Some((begin, _len)) = lengths.chunk_info(index, chunk_idx) {
4930 ct.chunk_received(index, begin);
4931 }
4932 }
4933 }
4934
4935 self.downloaded += data.len() as u64;
4936 self.total_download += data.len() as u64 + 13; self.last_download = now_unix();
4938 self.need_save_resume = true;
4939
4940 self.verify_and_mark_piece(index).await;
4942
4943 if let Some(ref ct) = self.chunk_tracker
4945 && !ct.has_piece(index)
4946 {
4947 self.ban_web_seed(&url);
4948 return;
4949 }
4950
4951 self.assign_pieces_to_web_seeds();
4952 }
4953
4954 pub(crate) fn handle_web_seed_error(&mut self, url: &str, piece: u32, message: &str) {
4955 self.web_seed_in_flight.remove(&piece);
4956 warn!(%url, piece, %message, "web seed error");
4957 self.assign_pieces_to_web_seeds();
4958 }
4959
4960 pub(crate) fn handle_web_seed_progress(
4968 &mut self,
4969 url: &str,
4970 bytes: u64,
4971 rate_bps: u64,
4972 error: Option<String>,
4973 ) {
4974 let now_unix = std::time::SystemTime::now()
4975 .duration_since(std::time::UNIX_EPOCH)
4976 .map_or(0, |d| d.as_secs());
4977 let entry = self
4978 .web_seed_stats
4979 .entry(url.to_owned())
4980 .or_insert_with(|| irontide_core::WebSeedStats {
4981 url: url.to_owned(),
4982 ..Default::default()
4983 });
4984 entry.downloaded_bytes = bytes;
4985 entry.last_rate_bps = rate_bps;
4986 entry.last_attempt_unix_secs = now_unix;
4987 if let Some(msg) = error {
4988 entry.state = irontide_core::WebSeedState::Errored;
4989 entry.last_error = Some(msg);
4990 entry.consecutive_failures = entry.consecutive_failures.saturating_add(1);
4991 let attempt = entry.consecutive_failures.saturating_sub(1);
4993 let secs = self
4994 .config
4995 .web_seed_retry_base_secs
4996 .saturating_mul(self.config.web_seed_retry_factor.saturating_pow(attempt))
4997 .min(self.config.web_seed_retry_cap_secs);
4998 entry.next_retry_unix_secs = Some(now_unix + secs);
4999 } else {
5000 entry.state = irontide_core::WebSeedState::Active;
5001 entry.consecutive_failures = 0;
5002 entry.next_retry_unix_secs = None;
5003 }
5004 self.need_save_resume = true;
5005 }
5006
5007 pub(crate) fn ban_web_seed(&mut self, url: &str) {
5008 warn!(%url, "banning web seed due to hash failure");
5009 self.banned_web_seeds.insert(url.to_owned());
5010
5011 if let Some(cmd_tx) = self.web_seeds.remove(url) {
5013 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::Shutdown);
5014 }
5015
5016 self.web_seed_in_flight.retain(|_, v| v != url);
5018
5019 post_alert(
5020 &self.alert_tx,
5021 &self.alert_mask,
5022 AlertKind::WebSeedBanned {
5023 info_hash: self.info_hash,
5024 url: url.to_owned(),
5025 },
5026 );
5027 }
5028
5029 async fn shutdown_web_seeds(&mut self) {
5030 for (_, cmd_tx) in self.web_seeds.drain() {
5031 let _ = cmd_tx.send(crate::web_seed::WebSeedCommand::Shutdown).await;
5032 }
5033 self.web_seed_in_flight.clear();
5034 }
5035
5036 fn refresh_peer_rates(&mut self) {
5038 self.cached_peer_rates.clear();
5039 self.cached_peer_rates.reserve(self.peers.len());
5040 for (&addr, p) in &self.peers {
5041 self.cached_peer_rates.insert(addr, p.pipeline.ewma_rate());
5042 }
5043 }
5044
5045 fn update_peer_rates(&mut self) {
5048 for peer in self.peers.values_mut() {
5049 peer.download_rate = peer.download_bytes_window / 2;
5050 peer.upload_rate = peer.upload_bytes_window / 2;
5051 peer.download_bytes_window = 0;
5052 peer.upload_bytes_window = 0;
5053 }
5054
5055 let aggregate_download: u64 = self.peers.values().map(|p| p.download_rate).sum();
5057 if aggregate_download > self.peak_download_rate {
5058 self.peak_download_rate = aggregate_download;
5059 }
5060 }
5061
5062 async fn run_choker(&mut self) {
5063 let peer_infos: Vec<ChokerPeerInfo> = self
5064 .peers
5065 .values()
5066 .map(|p| ChokerPeerInfo {
5067 addr: p.addr,
5068 download_rate: p.download_rate,
5069 upload_rate: p.upload_rate,
5070 interested: p.peer_interested,
5071 upload_only: p.upload_only,
5072 is_seed: p.upload_only
5073 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
5074 })
5075 .collect();
5076
5077 let decision = self.choker.decide(&peer_infos);
5078
5079 for addr in &decision.to_unchoke {
5080 if let Some(peer) = self.peers.get_mut(addr)
5081 && peer.am_choking
5082 {
5083 peer.am_choking = false;
5084 if peer.am_unchoke_started_at.is_none() {
5086 peer.am_unchoke_started_at = Some(Instant::now());
5087 }
5088 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(false));
5089 }
5090 }
5091
5092 for addr in &decision.to_choke {
5093 if let Some(peer) = self.peers.get_mut(addr)
5094 && !peer.am_choking
5095 {
5096 if peer.supports_fast {
5097 let pending: Vec<(u32, u32, u32)> = peer.incoming_requests.drain(..).collect();
5098 for (index, begin, length) in pending {
5099 let _ = peer.cmd_tx.try_send(PeerCommand::RejectRequest {
5100 index,
5101 begin,
5102 length,
5103 });
5104 }
5105 }
5106 peer.am_choking = true;
5107 if let Some(start) = peer.am_unchoke_started_at.take() {
5109 peer.unchoke_duration_total += start.elapsed();
5110 }
5111 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(true));
5112 }
5113 }
5114
5115 self.serve_incoming_requests().await;
5117
5118 if self.state == TorrentState::Downloading {
5123 let zombie_threshold = Duration::from_secs(30);
5124 let zombies: Vec<SocketAddr> = self
5125 .peers
5126 .values()
5127 .filter(|p| {
5128 p.bitfield.count_ones() == 0 && p.connected_at.elapsed() > zombie_threshold
5129 })
5130 .map(|p| p.addr)
5131 .collect();
5132
5133 for &addr in &zombies {
5134 debug!(%addr, "disconnecting zombie peer (empty bitfield after 30s)");
5135 self.disconnect_peer(addr, "zombie peer (empty bitfield)");
5136 }
5137 if !zombies.is_empty() {
5138 self.recalc_max_in_flight();
5139 }
5140 }
5141 }
5142
5143 fn rotate_optimistic(&mut self) {
5144 let peer_infos: Vec<ChokerPeerInfo> = self
5145 .peers
5146 .values()
5147 .map(|p| ChokerPeerInfo {
5148 addr: p.addr,
5149 download_rate: p.download_rate,
5150 upload_rate: p.upload_rate,
5151 interested: p.peer_interested,
5152 upload_only: p.upload_only,
5153 is_seed: p.upload_only
5154 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
5155 })
5156 .collect();
5157
5158 self.choker.rotate_optimistic(&peer_infos);
5159 }
5160
5161 fn handle_i2p_incoming(&mut self, stream: crate::i2p::SamStream) {
5167 if self.peers.len() >= self.effective_max_connections() {
5168 return;
5169 }
5170
5171 let synthetic_addr = self.next_i2p_synthetic_addr();
5172
5173 let remote_dest = stream.remote_destination().clone();
5174 let dest_preview = {
5175 let b64 = remote_dest.to_base64();
5176 if b64.len() >= 8 {
5177 b64[..8].to_string()
5178 } else {
5179 b64
5180 }
5181 };
5182 self.i2p_destinations.insert(synthetic_addr, remote_dest);
5183 let tcp_stream = stream.into_inner();
5184
5185 self.spawn_peer_from_stream(synthetic_addr, tcp_stream);
5186
5187 debug!(dest = %dest_preview, addr = %synthetic_addr, "accepted I2P peer");
5188 }
5189
5190 #[allow(dead_code)] fn add_i2p_peer(
5193 &mut self,
5194 dest: crate::i2p::I2pDestination,
5195 source: PeerSource,
5196 ) -> Option<SocketAddr> {
5197 if self.i2p_destinations.values().any(|d| d == &dest) {
5199 return None;
5200 }
5201 let addr = self.next_i2p_synthetic_addr();
5202 self.i2p_destinations.insert(addr, dest);
5203 if let Some(ref ps) = self.peer_states {
5205 ps.add_if_not_seen(addr, source);
5206 }
5207 Some(addr)
5208 }
5209
5210 fn next_i2p_synthetic_addr(&mut self) -> SocketAddr {
5216 self.i2p_peer_counter = self.i2p_peer_counter.wrapping_add(1);
5217 let a = ((self.i2p_peer_counter >> 16) & 0x0F) as u8 | 0xF0;
5218 let b = ((self.i2p_peer_counter >> 8) & 0xFF) as u8;
5219 let c = (self.i2p_peer_counter & 0xFF) as u8;
5220 SocketAddr::new(
5221 std::net::IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, 1)),
5222 (self.i2p_peer_counter & 0xFFFF) as u16,
5223 )
5224 }
5225}
5226
5227pub(crate) fn is_i2p_synthetic_addr(addr: &SocketAddr) -> bool {
5229 match addr {
5230 SocketAddr::V4(v4) => v4.ip().octets()[0] & 0xF0 == 0xF0,
5231 SocketAddr::V6(_) => false,
5232 }
5233}
5234
5235async fn accept_incoming(
5238 listener: &mut Option<Box<dyn crate::transport::TransportListener>>,
5239) -> std::io::Result<(crate::transport::BoxedStream, SocketAddr)> {
5240 match listener {
5241 Some(l) => l.accept().await,
5242 None => std::future::pending().await,
5243 }
5244}
5245
5246async fn accept_i2p(
5249 rx: &mut Option<mpsc::Receiver<crate::i2p::SamStream>>,
5250) -> Option<crate::i2p::SamStream> {
5251 match rx {
5252 Some(rx) => rx.recv().await,
5253 None => std::future::pending().await,
5254 }
5255}
5256
5257pub(crate) fn serve_hashes(
5268 meta_v2: Option<&irontide_core::TorrentMetaV2>,
5269 version: irontide_core::TorrentVersion,
5270 lengths: Option<&Lengths>,
5271 request: &irontide_core::HashRequest,
5272) -> Option<Vec<irontide_core::Id32>> {
5273 let meta_v2 = match meta_v2 {
5275 Some(m) if version != irontide_core::TorrentVersion::V1Only => m,
5276 _ => return None,
5277 };
5278
5279 let piece_hashes = meta_v2.file_piece_hashes(&request.file_root)?;
5281
5282 let lengths = lengths?;
5284
5285 let blocks_per_piece = (meta_v2.info.piece_length / u64::from(lengths.chunk_size())) as u32;
5290 let num_pieces = piece_hashes.len() as u32;
5291 let num_blocks = num_pieces.saturating_mul(blocks_per_piece);
5292
5293 if !irontide_core::validate_hash_request(request, num_blocks, num_pieces) {
5294 return None;
5295 }
5296
5297 let piece_layer_base = blocks_per_piece.trailing_zeros();
5300 if request.base != piece_layer_base {
5301 return None;
5302 }
5303
5304 let start = request.index as usize;
5306 let end = (start + request.count as usize).min(piece_hashes.len());
5307 let mut hashes: Vec<irontide_core::Id32> = piece_hashes[start..end].to_vec();
5308
5309 if request.proof_layers > 0 && !piece_hashes.is_empty() {
5317 let tree = irontide_core::MerkleTree::from_leaves(&piece_hashes);
5318 let full_proof = tree.proof_path(start);
5319 let subtree_depth = if request.count > 1 {
5321 (request.count as usize)
5322 .next_power_of_two()
5323 .trailing_zeros() as usize
5324 } else {
5325 0
5326 };
5327 let available = full_proof.len().saturating_sub(subtree_depth);
5328 let proof_count = (request.proof_layers as usize).min(available);
5329 hashes.extend_from_slice(&full_proof[subtree_depth..subtree_depth + proof_count]);
5330 }
5331
5332 Some(hashes)
5333}
5334
5335#[cfg(test)]
5340impl TorrentActor {
5341 pub(crate) fn for_throttle_test(num_pieces: u32, _throttle_ms: u64) -> Self {
5356 use irontide_storage::Bitfield;
5357
5358 let config = TorrentConfig {
5359 ..TorrentConfig::default()
5360 };
5361
5362 let info_hash = Id20([0u8; 20]);
5363 let our_peer_id = Id20([0u8; 20]);
5364
5365 let (_cmd_tx, cmd_rx) = mpsc::channel(1);
5366 let (event_tx, event_rx) = mpsc::channel(1);
5367 let (write_error_tx, write_error_rx) = mpsc::channel(1);
5368 let (verify_result_tx, verify_result_rx) = mpsc::channel(1);
5369 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
5370 let (piece_ready_tx, _piece_ready_rx) = broadcast::channel(1);
5371 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(num_pieces));
5372 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
5373 let (alert_tx, _alert_rx) = broadcast::channel(64);
5374 let (_disk_mgr_tx, _disk_mgr_rx) = mpsc::channel::<crate::disk::DiskJob>(1);
5375
5376 let stream_read_semaphore = Arc::new(tokio::sync::Semaphore::new(8));
5377 let alert_mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
5378
5379 let (disk_manager, _disk_join) =
5381 crate::disk::DiskManagerHandle::new(crate::disk::DiskConfig::default());
5382
5383 let ban_manager = Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
5384 crate::ban::BanConfig::default(),
5385 )));
5386 let ip_filter = Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
5387
5388 let upload_bucket = crate::rate_limiter::TokenBucket::new(0);
5389 let download_bucket = Arc::new(parking_lot::Mutex::new(
5390 crate::rate_limiter::TokenBucket::new(0),
5391 ));
5392 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(0, 0, 0, 0, 0, 0);
5393
5394 let dht_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5395 let dht_v6_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5396 let factory = Arc::new(crate::transport::NetworkFactory::tokio());
5397
5398 let we_have = Bitfield::new(num_pieces);
5402 let mut wanted = Bitfield::new(num_pieces);
5403 for i in 0..num_pieces {
5404 wanted.set(i);
5405 }
5406 let atomic_states = Arc::new(crate::piece_reservation::AtomicPieceStates::new(
5407 num_pieces, &we_have, &wanted,
5408 ));
5409
5410 let (order_map_tx, _order_map_rx_seed) =
5411 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
5412
5413 Self {
5414 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(0),
5415 config,
5416 info_hash,
5417 our_peer_id,
5418 state: TorrentState::Downloading,
5419 disk: None,
5420 disk_manager,
5421 chunk_tracker: None,
5422 lengths: None,
5423 num_pieces,
5424 file_priorities: Vec::new(),
5425 wanted_pieces: Bitfield::new(num_pieces),
5426 end_game: EndGame::new(),
5427 streaming_pieces: BTreeSet::new(),
5428 time_critical_pieces: BTreeSet::new(),
5429 streaming_cursors: Vec::new(),
5430 piece_ready_tx,
5431 have_watch_tx,
5432 have_watch_rx,
5433 stream_read_semaphore,
5434 peers: HashMap::new(),
5435 unchoke_durations: HashMap::new(),
5436 cached_peer_rates: FxHashMap::default(),
5437 refill_notify: Arc::new(tokio::sync::Notify::new()),
5438 atomic_states: Some(atomic_states),
5439 block_maps: None,
5440 steal_candidates: None,
5441 last_steal_populate: Instant::now(),
5442 piece_write_guards: None,
5443 soft_reap_buf: Vec::new(),
5444 eviction_history: std::collections::VecDeque::new(),
5445 force_immediate_choker_tick: false,
5446 piece_tracker: None,
5447 order_map_dirty: false,
5448 next_order_map_gen: 0,
5449 order_map_tx,
5450 piece_owner: vec![None; num_pieces as usize],
5451 peer_slab: crate::piece_reservation::PeerSlab::new(),
5452 priority_pieces: BTreeSet::new(),
5453 max_in_flight: 512,
5454 reservation_notify: None,
5455 last_tick_dispatch_state: None,
5456 choker: Choker::new(4),
5457 user_seed_mode: false,
5458 user_forced: false,
5459 max_connections: 0,
5460 peer_states: None,
5461 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
5462 connect_permits: HashMap::new(),
5463 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
5464 std::collections::HashMap::new(),
5465 )),
5466 connect_rx: None,
5467 metadata_downloader: None,
5468 meta: None,
5469 cached_files: None,
5470 downloaded: 0,
5471 uploaded: 0,
5472 checking_progress: 0.0,
5473 total_download: 0,
5474 total_upload: 0,
5475 total_failed_bytes: 0,
5476 total_redundant_bytes: 0,
5477 added_time: 0,
5478 completed_time: 0,
5479 last_download: 0,
5480 last_upload: 0,
5481 last_seen_complete: 0,
5482 active_duration: 0,
5483 finished_duration: 0,
5484 seeding_duration: 0,
5485 active_since: None,
5486 state_duration_since: None,
5487 started_at: Instant::now(),
5488 moving_storage: false,
5489 has_incoming: false,
5490 need_save_resume: false,
5491 error: String::new(),
5492 error_file: -1,
5493 cmd_rx,
5494 event_tx,
5495 event_rx,
5496 write_error_rx,
5497 write_error_tx,
5498 verify_result_rx,
5499 verify_result_tx,
5500 pending_verify: HashSet::new(),
5501 piece_generations: vec![0u64; num_pieces as usize],
5502 hash_result_rx,
5503 hash_result_tx,
5504 listener: None,
5505 utp_socket: None,
5506 utp_socket_v6: None,
5507 tracker_manager: TrackerManager::empty(info_hash, our_peer_id, 0, 0, false),
5508 tracker_result_rx: None,
5509 dht_rx,
5510 dht_v6_rx,
5511 dht_enabled: false,
5512 dht_peers_rx: None,
5513 dht_v6_peers_rx: None,
5514 dht_v6_empty_count: 0,
5515 dht_v6_last_retry: None,
5516 alert_tx,
5517 alert_mask,
5518 upload_bucket,
5519 download_bucket,
5520 global_upload_bucket: None,
5521 global_download_bucket: None,
5522 slot_tuner: crate::slot_tuner::SlotTuner::disabled(4),
5523 upload_bytes_interval: 0,
5524 peak_download_rate: 0,
5525 web_seeds: HashMap::new(),
5526 banned_web_seeds: HashSet::new(),
5527 web_seed_in_flight: HashMap::new(),
5528 web_seed_stats: HashMap::new(),
5529 pex_peer_count: 0,
5530 lsd_peer_count: 0,
5531 super_seed: None,
5532 have_broadcast_tx,
5533 suggested_to_peers: HashMap::new(),
5534 predictive_have_sent: HashSet::new(),
5535 ban_manager,
5536 piece_contributors: HashMap::new(),
5537 parole_pieces: HashMap::new(),
5538 ip_filter,
5539 external_ip: None,
5540 share_lru: std::collections::VecDeque::new(),
5541 share_max_pieces: 0,
5542 plugins: Arc::new(Vec::new()),
5543 hash_picker: None,
5544 version: irontide_core::TorrentVersion::V1Only,
5545 meta_v2: None,
5546 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
5547 dht_v2_peers_rx: None,
5548 dht_v6_v2_peers_rx: None,
5549 magnet_selected_files: None,
5550 sam_session: None,
5551 i2p_accept_rx: None,
5552 i2p_peer_counter: 0,
5553 i2p_destinations: HashMap::new(),
5554 ssl_manager: None,
5555 rate_limiter_set,
5556 auto_sequential_active: false,
5557 factory,
5558 hash_pool_ref: None,
5559 connect_attempts: 0,
5560 connect_failures: 0,
5561 choke_rotations: 0,
5562 inflight_started: Vec::new(),
5563 completed_piece_times: std::collections::VecDeque::new(),
5564 piece_steals: 0,
5565 holepunch_relayed: 0,
5566 holepunch_relay_rate: HashMap::new(),
5567 holepunch_cooldowns: HashMap::new(),
5568 holepunch_pending: Vec::new(),
5569 counters: Arc::new(crate::stats::SessionCounters::new()),
5570 }
5571 }
5572}
5573
5574#[cfg(test)]
5579mod tests {
5580 use super::*;
5581 use bytes::Bytes;
5582 use futures::{SinkExt, StreamExt};
5583 use irontide_wire::{ExtHandshake, Handshake, Message, MessageCodec};
5584 use std::time::Duration;
5585 use tokio::io::{AsyncReadExt, AsyncWriteExt};
5586 use tokio::net::TcpListener;
5587 use tokio_util::codec::{FramedRead, FramedWrite};
5588
5589 #[test]
5592 fn initial_unchoke_slots_unlimited_returns_default_four() {
5593 assert_eq!(initial_unchoke_slots(-1), 4);
5594 }
5595
5596 #[test]
5597 fn initial_unchoke_slots_capped_returns_value() {
5598 assert_eq!(initial_unchoke_slots(1), 1);
5599 assert_eq!(initial_unchoke_slots(4), 4);
5600 assert_eq!(initial_unchoke_slots(16), 16);
5601 }
5602
5603 fn make_test_torrent(data: &[u8], piece_length: u64) -> TorrentMetaV1 {
5607 use serde::Serialize;
5608
5609 #[derive(Serialize)]
5610 struct Info<'a> {
5611 length: u64,
5612 name: &'a str,
5613 #[serde(rename = "piece length")]
5614 piece_length: u64,
5615 #[serde(with = "serde_bytes")]
5616 pieces: &'a [u8],
5617 }
5618
5619 #[derive(Serialize)]
5620 struct Torrent<'a> {
5621 info: Info<'a>,
5622 }
5623
5624 let mut pieces = Vec::new();
5625 let mut offset = 0;
5626 while offset < data.len() {
5627 let end = (offset + piece_length as usize).min(data.len());
5628 let hash = irontide_core::sha1(&data[offset..end]);
5629 pieces.extend_from_slice(hash.as_bytes());
5630 offset = end;
5631 }
5632
5633 let t = Torrent {
5634 info: Info {
5635 length: data.len() as u64,
5636 name: "test",
5637 piece_length,
5638 pieces: &pieces,
5639 },
5640 };
5641
5642 let bytes = irontide_bencode::to_bytes(&t).unwrap();
5643 torrent_from_bytes(&bytes).unwrap()
5644 }
5645
5646 fn test_config() -> TorrentConfig {
5647 TorrentConfig {
5648 listen_port: 0, max_peers: 200,
5650 target_request_queue: 5,
5651 download_dir: std::path::PathBuf::from("/tmp"),
5652 enable_dht: false,
5653 enable_pex: false,
5654 enable_fast: false,
5655 seed_ratio_limit: None,
5656 seed_time_limit_secs: None,
5657 inactive_seed_time_limit_secs: None,
5658 strict_end_game: true,
5659 upload_rate_limit: 0,
5660 download_rate_limit: 0,
5661 max_uploads_per_torrent: -1,
5662 encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
5663 enable_utp: false,
5664 enable_web_seed: true,
5665 enable_holepunch: false,
5666 enable_bep40_eviction: true,
5667 max_web_seeds: 4,
5668 web_seed_retry_base_secs: 10,
5669 web_seed_retry_factor: 6,
5670 web_seed_retry_cap_secs: 3600,
5671 web_seed_max_failures: 10,
5672 super_seeding: false,
5673 upload_only_announce: true,
5674 hashing_threads: 2,
5675 sequential_download: false,
5676 initial_picker_threshold: 4,
5677 whole_pieces_threshold: 20,
5678 snub_timeout_secs: 15,
5679 readahead_pieces: 8,
5680 streaming_timeout_escalation: true,
5681 max_concurrent_stream_reads: 8,
5682 proxy: crate::proxy::ProxyConfig::default(),
5683 anonymous_mode: false,
5684 share_mode: false,
5685 enable_i2p: false,
5686 allow_i2p_mixed: false,
5687 ssl_listen_port: 0,
5688 seed_choking_algorithm: crate::choker::SeedChokingAlgorithm::FastestUpload,
5689 choking_algorithm: crate::choker::ChokingAlgorithm::FixedSlots,
5690 piece_extent_affinity: true,
5691 suggest_mode: false,
5692 max_suggest_pieces: 10,
5693 predictive_piece_announce_ms: 0,
5694 mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
5695 auto_sequential: true,
5696 storage_mode: irontide_core::StorageMode::Auto,
5697 preallocate_mode: None,
5698 block_request_timeout_secs: 60,
5699 enable_lsd: false,
5700 force_proxy: false,
5701 steal_threshold_ratio: 10.0,
5702 steal_threshold_endgame: 3.0,
5703 peer_read_timeout_secs: 0, peer_write_timeout_secs: 0, data_contribution_timeout_secs: 0, pass0_grace_secs: 60,
5708 proactive_evictions_per_minute_limit: 30,
5709 eviction_ban_duration_secs: 600,
5710 eviction_ban_set_cap: 1024,
5711 choke_rotation_max_evictions: 0, max_concurrent_connects: 128,
5713 connect_soft_timeout: 3,
5714 dispatch_backlog_cap: 8,
5715 event_backlog_cap: 32,
5716 peer_writer_channel_cap: 1024,
5717 use_actor_dispatch: true,
5718 web_seed_progress_throttle_ms: 250,
5719 url_security: crate::url_guard::UrlSecurityConfig::default(),
5720 peer_connect_timeout: 2,
5721 peer_dscp: 0x08,
5722 initial_queue_depth: 128,
5723 max_request_queue_depth: 250,
5724 request_queue_time: 3.0,
5725 max_metadata_size: 4 * 1024 * 1024,
5726 max_message_size: 16 * 1024 * 1024,
5727 max_piece_length: 32 * 1024 * 1024,
5728 max_outstanding_requests: 500,
5729 max_in_flight_pieces: 20,
5730 use_block_stealing: true,
5731 steal_stale_piece_secs: 2,
5732 fixed_pipeline_depth: 128,
5733 lock_warn_threshold_ms: 0, filesystem_direct_io: false,
5735 category: None,
5736 tags: Vec::new(),
5737 }
5738 }
5739
5740 fn make_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
5741 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
5742 Arc::new(MemoryStorage::new(lengths))
5743 }
5744
5745 fn make_seeded_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
5746 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
5747 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
5748 let num_pieces = lengths.num_pieces();
5750 for p in 0..num_pieces {
5751 let piece_size = lengths.piece_size(p) as usize;
5752 let offset = lengths.piece_offset(p) as usize;
5753 let end = offset + piece_size;
5754 storage.write_chunk(p, 0, &data[offset..end]).unwrap();
5755 }
5756 storage
5757 }
5758
5759 fn test_alert_channel() -> (broadcast::Sender<Alert>, Arc<AtomicU32>) {
5760 let (tx, _) = broadcast::channel(64);
5761 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
5762 (tx, mask)
5763 }
5764
5765 fn test_ban_manager() -> irontide_session_types::SharedBanManager {
5766 Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
5767 crate::ban::BanConfig::default(),
5768 )))
5769 }
5770
5771 fn test_ip_filter() -> irontide_session_types::SharedIpFilter {
5772 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()))
5773 }
5774
5775 fn test_disk_manager() -> (DiskManagerHandle, tokio::task::JoinHandle<()>) {
5776 DiskManagerHandle::new(crate::disk::DiskConfig::default())
5777 }
5778
5779 async fn test_register_disk(
5780 info_hash: Id20,
5781 storage: Arc<dyn TorrentStorage>,
5782 ) -> (DiskHandle, DiskManagerHandle, tokio::task::JoinHandle<()>) {
5783 let (dm, join) = test_disk_manager();
5784 let dh = dm.register_torrent(info_hash, storage).await;
5785 (dh, dm, join)
5786 }
5787
5788 fn test_dht_rx() -> irontide_dht::DhtReceiver {
5791 let bx = irontide_dht::DhtBroadcast::new(None);
5794 bx.subscribe()
5795 }
5796
5797 const HANDSHAKE_SIZE: usize = 68;
5799
5800 #[tokio::test]
5803 async fn create_from_torrent() {
5804 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
5807 let config = test_config();
5808
5809 let (atx, amask) = test_alert_channel();
5810 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5811 let handle = TorrentHandle::from_torrent(
5812 meta,
5813 irontide_core::TorrentVersion::V1Only,
5814 None,
5815 dh,
5816 dm,
5817 config,
5818 test_dht_rx(),
5819 test_dht_rx(),
5820 None,
5821 None,
5822 crate::slot_tuner::SlotTuner::disabled(4),
5823 atx,
5824 amask,
5825 None,
5826 None,
5827 test_ban_manager(),
5828 test_ip_filter(),
5829 Arc::new(Vec::new()),
5830 None,
5831 None,
5832 Arc::new(crate::transport::NetworkFactory::tokio()),
5833 None, Arc::new(crate::stats::SessionCounters::new()),
5835 )
5836 .await
5837 .unwrap();
5838
5839 let stats = handle.stats().await.unwrap();
5840 assert_eq!(stats.state, TorrentState::Downloading);
5841 assert_eq!(stats.pieces_total, 2);
5842 assert_eq!(stats.pieces_have, 0);
5843 assert_eq!(stats.peers_connected, 0);
5844
5845 handle.shutdown().await.unwrap();
5846 }
5847
5848 async fn started_test_handle() -> (TorrentHandle, Vec<String>, tokio::task::JoinHandle<()>) {
5857 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
5859 let expected_hex: Vec<String> =
5860 meta.info.pieces.chunks_exact(20).map(hex::encode).collect();
5861 let storage = make_storage(&data, 16384);
5862 let config = test_config();
5863
5864 let (atx, amask) = test_alert_channel();
5865 let (dh, dm, dj) = test_register_disk(meta.info_hash, storage).await;
5866 let handle = TorrentHandle::from_torrent(
5867 meta,
5868 irontide_core::TorrentVersion::V1Only,
5869 None,
5870 dh,
5871 dm,
5872 config,
5873 test_dht_rx(),
5874 test_dht_rx(),
5875 None,
5876 None,
5877 crate::slot_tuner::SlotTuner::disabled(4),
5878 atx,
5879 amask,
5880 None,
5881 None,
5882 test_ban_manager(),
5883 test_ip_filter(),
5884 Arc::new(Vec::new()),
5885 None,
5886 None,
5887 Arc::new(crate::transport::NetworkFactory::tokio()),
5888 None,
5889 Arc::new(crate::stats::SessionCounters::new()),
5890 )
5891 .await
5892 .unwrap();
5893 (handle, expected_hex, dj)
5894 }
5895
5896 #[tokio::test]
5903 async fn take_resume_if_dirty_is_atomic_capture_and_clear() {
5904 let (handle, _expected_hex, _dj) = started_test_handle().await;
5905
5906 handle.set_tags(vec!["m245".to_string()]).await.unwrap();
5910
5911 let first = handle.take_resume_if_dirty().await.unwrap();
5912 assert!(first.is_some(), "dirty torrent must yield resume data");
5913
5914 let second = handle.take_resume_if_dirty().await.unwrap();
5915 assert!(
5916 second.is_none(),
5917 "flag was cleared atomically in the same take — no second capture"
5918 );
5919
5920 handle.shutdown().await.unwrap();
5921 }
5922
5923 #[tokio::test]
5929 async fn mark_resume_dirty_restores_capture_after_write_failure() {
5930 let (handle, _expected_hex, _dj) = started_test_handle().await;
5931
5932 handle.set_tags(vec!["m245".to_string()]).await.unwrap();
5933
5934 let captured = handle.take_resume_if_dirty().await.unwrap();
5935 assert!(captured.is_some(), "dirty torrent captured once");
5936
5937 let between = handle.take_resume_if_dirty().await.unwrap();
5939 assert!(between.is_none(), "take cleared the flag");
5940
5941 handle.mark_resume_dirty().await.unwrap();
5943
5944 let recaptured = handle.take_resume_if_dirty().await.unwrap();
5945 assert!(
5946 recaptured.is_some(),
5947 "re-dirtied torrent must re-capture — no lost resume update"
5948 );
5949
5950 handle.shutdown().await.unwrap();
5951 }
5952
5953 #[tokio::test]
5960 async fn get_piece_hashes_hex_parity_and_windowing() {
5961 let (handle, expected_hex, _dj) = started_test_handle().await;
5962 assert_eq!(expected_hex.len(), 2, "2-piece test torrent");
5963
5964 let all = handle.get_piece_hashes(0, 1000).await.unwrap();
5966 assert_eq!(
5967 all, expected_hex,
5968 "hex output must match the raw piece hashes"
5969 );
5970
5971 let windowed = handle.get_piece_hashes(1, 1).await.unwrap();
5973 assert_eq!(windowed, vec![expected_hex[1].clone()]);
5974
5975 let first = handle.get_piece_hashes(0, 1).await.unwrap();
5977 assert_eq!(first, vec![expected_hex[0].clone()]);
5978
5979 let past = handle.get_piece_hashes(99, 5).await.unwrap();
5981 assert!(past.is_empty(), "offset past end yields empty");
5982
5983 let clamped = handle.get_piece_hashes(1, 1000).await.unwrap();
5985 assert_eq!(clamped, vec![expected_hex[1].clone()]);
5986
5987 handle.shutdown().await.unwrap();
5988 }
5989
5990 #[tokio::test]
5993 async fn create_from_magnet() {
5994 let magnet = Magnet {
5995 info_hashes: irontide_core::InfoHashes::v1_only(
5996 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
5997 ),
5998 display_name: Some("test".into()),
5999 trackers: vec![],
6000 peers: vec![],
6001 selected_files: None,
6002 };
6003 let config = test_config();
6004
6005 let (atx, amask) = test_alert_channel();
6006 let (dm, _dj) = test_disk_manager();
6007 let handle = TorrentHandle::from_magnet(
6008 magnet,
6009 dm,
6010 config,
6011 test_dht_rx(),
6012 test_dht_rx(),
6013 None,
6014 None,
6015 crate::slot_tuner::SlotTuner::disabled(4),
6016 atx,
6017 amask,
6018 None,
6019 None,
6020 test_ban_manager(),
6021 test_ip_filter(),
6022 Arc::new(Vec::new()),
6023 None,
6024 None,
6025 Arc::new(crate::transport::NetworkFactory::tokio()),
6026 None, Arc::new(crate::stats::SessionCounters::new()),
6028 )
6029 .await
6030 .unwrap();
6031
6032 let stats = handle.stats().await.unwrap();
6033 assert_eq!(stats.state, TorrentState::FetchingMetadata);
6034 assert_eq!(stats.pieces_total, 0);
6035
6036 handle.shutdown().await.unwrap();
6037 }
6038
6039 #[tokio::test]
6042 async fn add_peers_increases_available() {
6043 let data = vec![0xAB; 32768];
6044 let meta = make_test_torrent(&data, 16384);
6045 let storage = make_storage(&data, 16384);
6046 let config = test_config();
6047
6048 let (atx, amask) = test_alert_channel();
6049 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6050 let handle = TorrentHandle::from_torrent(
6051 meta,
6052 irontide_core::TorrentVersion::V1Only,
6053 None,
6054 dh,
6055 dm,
6056 config,
6057 test_dht_rx(),
6058 test_dht_rx(),
6059 None,
6060 None,
6061 crate::slot_tuner::SlotTuner::disabled(4),
6062 atx,
6063 amask,
6064 None,
6065 None,
6066 test_ban_manager(),
6067 test_ip_filter(),
6068 Arc::new(Vec::new()),
6069 None,
6070 None,
6071 Arc::new(crate::transport::NetworkFactory::tokio()),
6072 None, Arc::new(crate::stats::SessionCounters::new()),
6074 )
6075 .await
6076 .unwrap();
6077
6078 let listener1 = TcpListener::bind("127.0.0.1:0").await.unwrap();
6080 let addr1 = listener1.local_addr().unwrap();
6081 let listener2 = TcpListener::bind("127.0.0.1:0").await.unwrap();
6082 let addr2 = listener2.local_addr().unwrap();
6083
6084 handle
6085 .add_peers(vec![addr1, addr2], PeerSource::Tracker)
6086 .await
6087 .unwrap();
6088
6089 tokio::time::sleep(Duration::from_millis(100)).await;
6091
6092 let stats = handle.stats().await.unwrap();
6093 assert!(
6095 stats.peers_available + stats.peers_connected >= 2,
6096 "expected at least 2 peers known, got available={}, connected={}",
6097 stats.peers_available,
6098 stats.peers_connected
6099 );
6100
6101 handle.shutdown().await.unwrap();
6102 }
6103
6104 #[tokio::test]
6107 async fn stats_reporting() {
6108 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
6111 let config = test_config();
6112
6113 let (atx, amask) = test_alert_channel();
6114 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6115 let handle = TorrentHandle::from_torrent(
6116 meta,
6117 irontide_core::TorrentVersion::V1Only,
6118 None,
6119 dh,
6120 dm,
6121 config,
6122 test_dht_rx(),
6123 test_dht_rx(),
6124 None,
6125 None,
6126 crate::slot_tuner::SlotTuner::disabled(4),
6127 atx,
6128 amask,
6129 None,
6130 None,
6131 test_ban_manager(),
6132 test_ip_filter(),
6133 Arc::new(Vec::new()),
6134 None,
6135 None,
6136 Arc::new(crate::transport::NetworkFactory::tokio()),
6137 None, Arc::new(crate::stats::SessionCounters::new()),
6139 )
6140 .await
6141 .unwrap();
6142
6143 let stats = handle.stats().await.unwrap();
6144 assert_eq!(stats.state, TorrentState::Downloading);
6145 assert_eq!(stats.downloaded, 0);
6146 assert_eq!(stats.uploaded, 0);
6147 assert_eq!(stats.pieces_have, 0);
6148 assert_eq!(stats.pieces_total, 4);
6149 assert_eq!(stats.peers_connected, 0);
6150 assert_eq!(stats.peers_available, 0);
6151
6152 handle.shutdown().await.unwrap();
6153 }
6154
6155 #[tokio::test]
6158 async fn private_torrent_disables_dht_pex() {
6159 use serde::Serialize;
6161
6162 #[derive(Serialize)]
6163 struct Info<'a> {
6164 length: u64,
6165 name: &'a str,
6166 #[serde(rename = "piece length")]
6167 piece_length: u64,
6168 #[serde(with = "serde_bytes")]
6169 pieces: &'a [u8],
6170 private: i64,
6171 }
6172
6173 #[derive(Serialize)]
6174 struct Torrent<'a> {
6175 info: Info<'a>,
6176 }
6177
6178 let data = vec![0xAB; 16384];
6179 let hash = irontide_core::sha1(&data);
6180 let mut pieces = Vec::new();
6181 pieces.extend_from_slice(hash.as_bytes());
6182
6183 let t = Torrent {
6184 info: Info {
6185 length: data.len() as u64,
6186 name: "private_test",
6187 piece_length: 16384,
6188 pieces: &pieces,
6189 private: 1,
6190 },
6191 };
6192
6193 let bytes = irontide_bencode::to_bytes(&t).unwrap();
6194 let meta = torrent_from_bytes(&bytes).unwrap();
6195 assert_eq!(meta.info.private, Some(1));
6196
6197 let storage = make_storage(&data, 16384);
6198 let mut config = test_config();
6199 config.enable_dht = true;
6200 config.enable_pex = true;
6201
6202 let (atx, amask) = test_alert_channel();
6204 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6205 let handle = TorrentHandle::from_torrent(
6206 meta,
6207 irontide_core::TorrentVersion::V1Only,
6208 None,
6209 dh,
6210 dm,
6211 config,
6212 test_dht_rx(),
6213 test_dht_rx(),
6214 None,
6215 None,
6216 crate::slot_tuner::SlotTuner::disabled(4),
6217 atx,
6218 amask,
6219 None,
6220 None,
6221 test_ban_manager(),
6222 test_ip_filter(),
6223 Arc::new(Vec::new()),
6224 None,
6225 None,
6226 Arc::new(crate::transport::NetworkFactory::tokio()),
6227 None, Arc::new(crate::stats::SessionCounters::new()),
6229 )
6230 .await
6231 .unwrap();
6232
6233 let stats = handle.stats().await.unwrap();
6237 assert_eq!(stats.state, TorrentState::Downloading);
6238
6239 handle.shutdown().await.unwrap();
6240 }
6241
6242 #[tokio::test]
6245 async fn shutdown_cleanup() {
6246 let data = vec![0xAB; 16384];
6247 let meta = make_test_torrent(&data, 16384);
6248 let storage = make_storage(&data, 16384);
6249 let config = test_config();
6250
6251 let (atx, amask) = test_alert_channel();
6252 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6253 let handle = TorrentHandle::from_torrent(
6254 meta,
6255 irontide_core::TorrentVersion::V1Only,
6256 None,
6257 dh,
6258 dm,
6259 config,
6260 test_dht_rx(),
6261 test_dht_rx(),
6262 None,
6263 None,
6264 crate::slot_tuner::SlotTuner::disabled(4),
6265 atx,
6266 amask,
6267 None,
6268 None,
6269 test_ban_manager(),
6270 test_ip_filter(),
6271 Arc::new(Vec::new()),
6272 None,
6273 None,
6274 Arc::new(crate::transport::NetworkFactory::tokio()),
6275 None, Arc::new(crate::stats::SessionCounters::new()),
6277 )
6278 .await
6279 .unwrap();
6280
6281 handle.shutdown().await.unwrap();
6282
6283 tokio::time::sleep(Duration::from_millis(50)).await;
6285 let result = handle.stats().await;
6286 assert!(result.is_err());
6287 }
6288
6289 #[tokio::test]
6292 async fn duplicate_peers_ignored() {
6293 let data = vec![0xAB; 16384];
6294 let meta = make_test_torrent(&data, 16384);
6295 let storage = make_storage(&data, 16384);
6296 let config = test_config();
6297
6298 let (atx, amask) = test_alert_channel();
6299 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6300 let handle = TorrentHandle::from_torrent(
6301 meta,
6302 irontide_core::TorrentVersion::V1Only,
6303 None,
6304 dh,
6305 dm,
6306 config,
6307 test_dht_rx(),
6308 test_dht_rx(),
6309 None,
6310 None,
6311 crate::slot_tuner::SlotTuner::disabled(4),
6312 atx,
6313 amask,
6314 None,
6315 None,
6316 test_ban_manager(),
6317 test_ip_filter(),
6318 Arc::new(Vec::new()),
6319 None,
6320 None,
6321 Arc::new(crate::transport::NetworkFactory::tokio()),
6322 None, Arc::new(crate::stats::SessionCounters::new()),
6324 )
6325 .await
6326 .unwrap();
6327
6328 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6330 let addr = listener.local_addr().unwrap();
6331 handle
6332 .add_peers(vec![addr, addr, addr], PeerSource::Tracker)
6333 .await
6334 .unwrap();
6335
6336 tokio::time::sleep(Duration::from_millis(100)).await;
6337 let stats = handle.stats().await.unwrap();
6338 assert!(
6340 stats.peers_available + stats.peers_connected <= 1,
6341 "expected at most 1 unique peer, got available={}, connected={}",
6342 stats.peers_available,
6343 stats.peers_connected
6344 );
6345
6346 handle.shutdown().await.unwrap();
6347 }
6348
6349 #[tokio::test]
6352 async fn cloned_handle_shares_actor() {
6353 let data = vec![0xAB; 16384];
6354 let meta = make_test_torrent(&data, 16384);
6355 let storage = make_storage(&data, 16384);
6356 let config = test_config();
6357
6358 let (atx, amask) = test_alert_channel();
6359 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6360 let handle = TorrentHandle::from_torrent(
6361 meta,
6362 irontide_core::TorrentVersion::V1Only,
6363 None,
6364 dh,
6365 dm,
6366 config,
6367 test_dht_rx(),
6368 test_dht_rx(),
6369 None,
6370 None,
6371 crate::slot_tuner::SlotTuner::disabled(4),
6372 atx,
6373 amask,
6374 None,
6375 None,
6376 test_ban_manager(),
6377 test_ip_filter(),
6378 Arc::new(Vec::new()),
6379 None,
6380 None,
6381 Arc::new(crate::transport::NetworkFactory::tokio()),
6382 None, Arc::new(crate::stats::SessionCounters::new()),
6384 )
6385 .await
6386 .unwrap();
6387 let handle2 = handle.clone();
6388
6389 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6391 let peer_addr = listener.local_addr().unwrap();
6392
6393 handle
6395 .add_peers(vec![peer_addr], PeerSource::Tracker)
6396 .await
6397 .unwrap();
6398
6399 tokio::time::sleep(Duration::from_millis(100)).await;
6400
6401 let stats = handle2.stats().await.unwrap();
6403 assert!(
6404 stats.peers_available + stats.peers_connected >= 1,
6405 "expected at least 1 peer known, got available={}, connected={}",
6406 stats.peers_available,
6407 stats.peers_connected
6408 );
6409
6410 handle.shutdown().await.unwrap();
6411 }
6412
6413 #[tokio::test]
6416 async fn peer_connect_and_disconnect_via_listener() {
6417 let data = vec![0xAB; 16384];
6418 let meta = make_test_torrent(&data, 16384);
6419 let info_hash = meta.info_hash;
6420 let storage = make_storage(&data, 16384);
6421
6422 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6424 let listen_addr = listener.local_addr().unwrap();
6425
6426 let config = TorrentConfig {
6427 listen_port: listen_addr.port(),
6428 ..test_config()
6429 };
6430
6431 drop(listener);
6433
6434 let (atx, amask) = test_alert_channel();
6435 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6436 let handle = TorrentHandle::from_torrent(
6437 meta,
6438 irontide_core::TorrentVersion::V1Only,
6439 None,
6440 dh,
6441 dm,
6442 config,
6443 test_dht_rx(),
6444 test_dht_rx(),
6445 None,
6446 None,
6447 crate::slot_tuner::SlotTuner::disabled(4),
6448 atx,
6449 amask,
6450 None,
6451 None,
6452 test_ban_manager(),
6453 test_ip_filter(),
6454 Arc::new(Vec::new()),
6455 None,
6456 None,
6457 Arc::new(crate::transport::NetworkFactory::tokio()),
6458 None, Arc::new(crate::stats::SessionCounters::new()),
6460 )
6461 .await
6462 .unwrap();
6463
6464 tokio::time::sleep(Duration::from_millis(50)).await;
6466
6467 let mut stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6469
6470 let remote_id = Id20::from_hex("1111111111111111111111111111111111111111").unwrap();
6472 let remote_hs = Handshake::new(info_hash, remote_id);
6473 stream.write_all(&remote_hs.to_bytes()).await.unwrap();
6474 stream.flush().await.unwrap();
6475
6476 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6477 stream.read_exact(&mut hs_buf).await.unwrap();
6478 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
6479 assert_eq!(their_hs.info_hash, info_hash);
6480
6481 tokio::time::sleep(Duration::from_millis(100)).await;
6483
6484 let stats = handle.stats().await.unwrap();
6485 assert_eq!(stats.peers_connected, 1);
6486
6487 drop(stream);
6489
6490 tokio::time::sleep(Duration::from_millis(200)).await;
6492
6493 let stats = handle.stats().await.unwrap();
6494 assert_eq!(stats.peers_connected, 0);
6495
6496 handle.shutdown().await.unwrap();
6497 }
6498
6499 #[tokio::test]
6505 async fn piece_download_and_verify() {
6506 let data = vec![0xCDu8; 16384];
6508 let meta = make_test_torrent(&data, 16384);
6509 let info_hash = meta.info_hash;
6510 let storage = make_storage(&data, 16384);
6511
6512 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6513 let listen_addr = listener.local_addr().unwrap();
6514 drop(listener);
6515
6516 let config = TorrentConfig {
6517 listen_port: listen_addr.port(),
6518 ..test_config()
6519 };
6520
6521 let (atx, amask) = test_alert_channel();
6522 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6523 let handle = TorrentHandle::from_torrent(
6524 meta,
6525 irontide_core::TorrentVersion::V1Only,
6526 None,
6527 dh,
6528 dm,
6529 config,
6530 test_dht_rx(),
6531 test_dht_rx(),
6532 None,
6533 None,
6534 crate::slot_tuner::SlotTuner::disabled(4),
6535 atx,
6536 amask,
6537 None,
6538 None,
6539 test_ban_manager(),
6540 test_ip_filter(),
6541 Arc::new(Vec::new()),
6542 None,
6543 None,
6544 Arc::new(crate::transport::NetworkFactory::tokio()),
6545 None, Arc::new(crate::stats::SessionCounters::new()),
6547 )
6548 .await
6549 .unwrap();
6550
6551 tokio::time::sleep(Duration::from_millis(50)).await;
6552
6553 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6555 let remote_id = Id20::from_hex("2222222222222222222222222222222222222222").unwrap();
6556
6557 let mock_data = data.clone();
6559 let mock_task = tokio::spawn(async move {
6560 let (reader, writer) = tokio::io::split(stream);
6561 let mut reader = reader;
6562 let mut writer = writer;
6563
6564 let hs = Handshake::new(info_hash, remote_id);
6566 writer.write_all(&hs.to_bytes()).await.unwrap();
6567 writer.flush().await.unwrap();
6568
6569 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6570 reader.read_exact(&mut hs_buf).await.unwrap();
6571
6572 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6574 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6575
6576 let _msg = framed_read.next().await;
6578
6579 let ext_hs = ExtHandshake::new();
6581 let payload = ext_hs.to_bytes().unwrap();
6582 framed_write
6583 .send(Message::Extended { ext_id: 0, payload })
6584 .await
6585 .unwrap();
6586
6587 let mut bf = Bitfield::new(1);
6589 bf.set(0);
6590 framed_write
6591 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6592 .await
6593 .unwrap();
6594
6595 framed_write.send(Message::Unchoke).await.unwrap();
6597
6598 while let Some(Ok(msg)) = framed_read.next().await {
6600 if let Message::Request {
6601 index,
6602 begin,
6603 length,
6604 } = msg
6605 {
6606 let start = begin as usize;
6607 let end = start + length as usize;
6608 let piece_data = &mock_data[start..end];
6609 framed_write
6610 .send(Message::Piece {
6611 index,
6612 begin,
6613 data_0: Bytes::copy_from_slice(piece_data),
6614 data_1: Bytes::new(),
6615 })
6616 .await
6617 .unwrap();
6618 }
6619 }
6620 });
6621
6622 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6624 loop {
6625 tokio::time::sleep(Duration::from_millis(100)).await;
6626 let stats = handle.stats().await.unwrap();
6627 if stats.state == TorrentState::Seeding {
6628 assert_eq!(stats.pieces_have, 1);
6629 assert_eq!(stats.pieces_total, 1);
6630 break;
6631 }
6632 if tokio::time::Instant::now() > deadline {
6633 let stats = handle.stats().await.unwrap();
6634 panic!(
6635 "download did not complete within 5s, state={:?}, have={}/{}",
6636 stats.state, stats.pieces_have, stats.pieces_total
6637 );
6638 }
6639 }
6640
6641 handle.shutdown().await.unwrap();
6642 mock_task.abort();
6643 }
6644
6645 #[tokio::test]
6648 async fn failed_piece_verification() {
6649 let data = vec![0xEEu8; 16384];
6651 let meta = make_test_torrent(&data, 16384);
6652 let info_hash = meta.info_hash;
6653 let storage = make_storage(&data, 16384);
6654
6655 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6656 let listen_addr = listener.local_addr().unwrap();
6657 drop(listener);
6658
6659 let config = TorrentConfig {
6660 listen_port: listen_addr.port(),
6661 ..test_config()
6662 };
6663
6664 let (atx, amask) = test_alert_channel();
6665 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6666 let handle = TorrentHandle::from_torrent(
6667 meta,
6668 irontide_core::TorrentVersion::V1Only,
6669 None,
6670 dh,
6671 dm,
6672 config,
6673 test_dht_rx(),
6674 test_dht_rx(),
6675 None,
6676 None,
6677 crate::slot_tuner::SlotTuner::disabled(4),
6678 atx,
6679 amask,
6680 None,
6681 None,
6682 test_ban_manager(),
6683 test_ip_filter(),
6684 Arc::new(Vec::new()),
6685 None,
6686 None,
6687 Arc::new(crate::transport::NetworkFactory::tokio()),
6688 None, Arc::new(crate::stats::SessionCounters::new()),
6690 )
6691 .await
6692 .unwrap();
6693
6694 tokio::time::sleep(Duration::from_millis(50)).await;
6695
6696 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6698 let remote_id = Id20::from_hex("3333333333333333333333333333333333333333").unwrap();
6699
6700 let correct_data = data.clone();
6701 let mock_task = tokio::spawn(async move {
6702 let (reader, writer) = tokio::io::split(stream);
6703
6704 let mut writer = writer;
6706 let mut reader = reader;
6707 let hs = Handshake::new(info_hash, remote_id);
6708 writer.write_all(&hs.to_bytes()).await.unwrap();
6709 writer.flush().await.unwrap();
6710
6711 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6712 reader.read_exact(&mut hs_buf).await.unwrap();
6713
6714 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6715 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6716
6717 let _msg = framed_read.next().await;
6719
6720 let ext_hs = ExtHandshake::new();
6722 let payload = ext_hs.to_bytes().unwrap();
6723 framed_write
6724 .send(Message::Extended { ext_id: 0, payload })
6725 .await
6726 .unwrap();
6727
6728 let mut bf = Bitfield::new(1);
6730 bf.set(0);
6731 framed_write
6732 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6733 .await
6734 .unwrap();
6735
6736 framed_write.send(Message::Unchoke).await.unwrap();
6738
6739 let mut request_count = 0u32;
6740 while let Some(Ok(msg)) = framed_read.next().await {
6741 if let Message::Request {
6742 index,
6743 begin,
6744 length,
6745 } = msg
6746 {
6747 request_count += 1;
6748 let piece_data = if request_count <= 1 {
6749 vec![0xFF; length as usize]
6751 } else {
6752 let start = begin as usize;
6754 let end = start + length as usize;
6755 correct_data[start..end].to_vec()
6756 };
6757 framed_write
6758 .send(Message::Piece {
6759 index,
6760 begin,
6761 data_0: Bytes::from(piece_data),
6762 data_1: Bytes::new(),
6763 })
6764 .await
6765 .unwrap();
6766 }
6767 }
6768 });
6769
6770 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6772 loop {
6773 tokio::time::sleep(Duration::from_millis(100)).await;
6774 let stats = handle.stats().await.unwrap();
6775 if stats.state == TorrentState::Seeding {
6776 assert_eq!(stats.pieces_have, 1);
6777 break;
6778 }
6779 if tokio::time::Instant::now() > deadline {
6780 let stats = handle.stats().await.unwrap();
6781 panic!(
6782 "download did not complete after retry within 5s, state={:?}, have={}",
6783 stats.state, stats.pieces_have,
6784 );
6785 }
6786 }
6787
6788 handle.shutdown().await.unwrap();
6789 mock_task.abort();
6790 }
6791
6792 #[tokio::test]
6795 async fn complete_transitions_state() {
6796 let data = vec![0xBBu8; 32768];
6798 let meta = make_test_torrent(&data, 16384);
6799 let info_hash = meta.info_hash;
6800 let storage = make_storage(&data, 16384);
6801
6802 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6803 let listen_addr = listener.local_addr().unwrap();
6804 drop(listener);
6805
6806 let config = TorrentConfig {
6807 listen_port: listen_addr.port(),
6808 ..test_config()
6809 };
6810
6811 let (atx, amask) = test_alert_channel();
6812 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6813 let handle = TorrentHandle::from_torrent(
6814 meta,
6815 irontide_core::TorrentVersion::V1Only,
6816 None,
6817 dh,
6818 dm,
6819 config,
6820 test_dht_rx(),
6821 test_dht_rx(),
6822 None,
6823 None,
6824 crate::slot_tuner::SlotTuner::disabled(4),
6825 atx,
6826 amask,
6827 None,
6828 None,
6829 test_ban_manager(),
6830 test_ip_filter(),
6831 Arc::new(Vec::new()),
6832 None,
6833 None,
6834 Arc::new(crate::transport::NetworkFactory::tokio()),
6835 None, Arc::new(crate::stats::SessionCounters::new()),
6837 )
6838 .await
6839 .unwrap();
6840
6841 tokio::time::sleep(Duration::from_millis(50)).await;
6842
6843 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6845 let remote_id = Id20::from_hex("4444444444444444444444444444444444444444").unwrap();
6846
6847 let mock_data = data.clone();
6848 let mock_task = tokio::spawn(async move {
6849 let (reader, writer) = tokio::io::split(stream);
6850 let mut writer = writer;
6851 let mut reader = reader;
6852
6853 let hs = Handshake::new(info_hash, remote_id);
6854 writer.write_all(&hs.to_bytes()).await.unwrap();
6855 writer.flush().await.unwrap();
6856
6857 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6858 reader.read_exact(&mut hs_buf).await.unwrap();
6859
6860 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6861 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6862
6863 let _msg = framed_read.next().await;
6865
6866 let ext_hs = ExtHandshake::new();
6868 let payload = ext_hs.to_bytes().unwrap();
6869 framed_write
6870 .send(Message::Extended { ext_id: 0, payload })
6871 .await
6872 .unwrap();
6873
6874 let mut bf = Bitfield::new(2);
6876 bf.set(0);
6877 bf.set(1);
6878 framed_write
6879 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6880 .await
6881 .unwrap();
6882
6883 framed_write.send(Message::Unchoke).await.unwrap();
6884
6885 while let Some(Ok(msg)) = framed_read.next().await {
6886 if let Message::Request {
6887 index,
6888 begin,
6889 length,
6890 } = msg
6891 {
6892 let abs_start = (index as usize * 16384) + begin as usize;
6893 let abs_end = abs_start + length as usize;
6894 let piece_data = &mock_data[abs_start..abs_end];
6895 framed_write
6896 .send(Message::Piece {
6897 index,
6898 begin,
6899 data_0: Bytes::copy_from_slice(piece_data),
6900 data_1: Bytes::new(),
6901 })
6902 .await
6903 .unwrap();
6904 }
6905 }
6906 });
6907
6908 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6909 loop {
6910 tokio::time::sleep(Duration::from_millis(100)).await;
6911 let stats = handle.stats().await.unwrap();
6912 if stats.state == TorrentState::Seeding {
6913 assert_eq!(stats.pieces_have, 2);
6914 assert_eq!(stats.pieces_total, 2);
6915 break;
6916 }
6917 if tokio::time::Instant::now() > deadline {
6918 let stats = handle.stats().await.unwrap();
6919 panic!(
6920 "expected Complete, got {:?}, have={}/{}",
6921 stats.state, stats.pieces_have, stats.pieces_total
6922 );
6923 }
6924 }
6925
6926 handle.shutdown().await.unwrap();
6927 mock_task.abort();
6928 }
6929
6930 #[tokio::test]
6933 async fn multi_chunk_piece_download() {
6934 let data = vec![0xAAu8; 32768];
6936 let meta = make_test_torrent(&data, 32768);
6937 let info_hash = meta.info_hash;
6938 let storage = make_storage(&data, 32768);
6939
6940 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6941 let listen_addr = listener.local_addr().unwrap();
6942 drop(listener);
6943
6944 let config = TorrentConfig {
6945 listen_port: listen_addr.port(),
6946 ..test_config()
6947 };
6948
6949 let (atx, amask) = test_alert_channel();
6950 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6951 let handle = TorrentHandle::from_torrent(
6952 meta,
6953 irontide_core::TorrentVersion::V1Only,
6954 None,
6955 dh,
6956 dm,
6957 config,
6958 test_dht_rx(),
6959 test_dht_rx(),
6960 None,
6961 None,
6962 crate::slot_tuner::SlotTuner::disabled(4),
6963 atx,
6964 amask,
6965 None,
6966 None,
6967 test_ban_manager(),
6968 test_ip_filter(),
6969 Arc::new(Vec::new()),
6970 None,
6971 None,
6972 Arc::new(crate::transport::NetworkFactory::tokio()),
6973 None, Arc::new(crate::stats::SessionCounters::new()),
6975 )
6976 .await
6977 .unwrap();
6978
6979 tokio::time::sleep(Duration::from_millis(50)).await;
6980
6981 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6982 let remote_id = Id20::from_hex("5555555555555555555555555555555555555555").unwrap();
6983
6984 let mock_data = data.clone();
6985 let mock_task = tokio::spawn(async move {
6986 let (reader, writer) = tokio::io::split(stream);
6987 let mut writer = writer;
6988 let mut reader = reader;
6989
6990 let hs = Handshake::new(info_hash, remote_id);
6991 writer.write_all(&hs.to_bytes()).await.unwrap();
6992 writer.flush().await.unwrap();
6993
6994 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6995 reader.read_exact(&mut hs_buf).await.unwrap();
6996
6997 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6998 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6999
7000 let _msg = framed_read.next().await;
7001
7002 let ext_hs = ExtHandshake::new();
7003 let payload = ext_hs.to_bytes().unwrap();
7004 framed_write
7005 .send(Message::Extended { ext_id: 0, payload })
7006 .await
7007 .unwrap();
7008
7009 let mut bf = Bitfield::new(1);
7010 bf.set(0);
7011 framed_write
7012 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7013 .await
7014 .unwrap();
7015
7016 framed_write.send(Message::Unchoke).await.unwrap();
7017
7018 while let Some(Ok(msg)) = framed_read.next().await {
7019 if let Message::Request {
7020 index: _,
7021 begin,
7022 length,
7023 } = msg
7024 {
7025 let start = begin as usize;
7026 let end = start + length as usize;
7027 framed_write
7028 .send(Message::Piece {
7029 index: 0,
7030 begin,
7031 data_0: Bytes::copy_from_slice(&mock_data[start..end]),
7032 data_1: Bytes::new(),
7033 })
7034 .await
7035 .unwrap();
7036 }
7037 }
7038 });
7039
7040 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7041 loop {
7042 tokio::time::sleep(Duration::from_millis(100)).await;
7043 let stats = handle.stats().await.unwrap();
7044 if stats.state == TorrentState::Seeding {
7045 assert_eq!(stats.pieces_have, 1);
7046 break;
7047 }
7048 assert!(
7049 tokio::time::Instant::now() <= deadline,
7050 "multi-chunk download did not complete within 5s"
7051 );
7052 }
7053
7054 handle.shutdown().await.unwrap();
7055 mock_task.abort();
7056 }
7057
7058 #[tokio::test]
7061 async fn seeder_leecher_integration() {
7062 let data = vec![0xDDu8; 32768]; let piece_length = 16384u64;
7065 let meta = make_test_torrent(&data, piece_length);
7066 let info_hash = meta.info_hash;
7067
7068 let seeder_storage = make_seeded_storage(&data, piece_length);
7070
7071 let seeder_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7077 let seeder_addr = seeder_listener.local_addr().unwrap();
7078
7079 let seeder_task = tokio::spawn(async move {
7080 let (stream, _addr) = seeder_listener.accept().await.unwrap();
7081 let (reader, writer) = tokio::io::split(stream);
7082 let mut writer = writer;
7083 let mut reader = reader;
7084
7085 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7087 reader.read_exact(&mut hs_buf).await.unwrap();
7088 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
7089 assert_eq!(their_hs.info_hash, info_hash);
7090
7091 let hs = Handshake::new(info_hash, PeerId::generate().0);
7092 writer.write_all(&hs.to_bytes()).await.unwrap();
7093 writer.flush().await.unwrap();
7094
7095 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7096 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7097
7098 let _msg = framed_read.next().await;
7100
7101 let ext_hs = ExtHandshake::new();
7103 let payload = ext_hs.to_bytes().unwrap();
7104 framed_write
7105 .send(Message::Extended { ext_id: 0, payload })
7106 .await
7107 .unwrap();
7108
7109 let mut bf = Bitfield::new(2);
7111 bf.set(0);
7112 bf.set(1);
7113 framed_write
7114 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7115 .await
7116 .unwrap();
7117
7118 framed_write.send(Message::Unchoke).await.unwrap();
7120
7121 while let Some(Ok(msg)) = framed_read.next().await {
7123 if let Message::Request {
7124 index,
7125 begin,
7126 length,
7127 } = msg
7128 {
7129 let piece_data = seeder_storage.read_chunk(index, begin, length).unwrap();
7130 framed_write
7131 .send(Message::Piece {
7132 index,
7133 begin,
7134 data_0: Bytes::from(piece_data),
7135 data_1: Bytes::new(),
7136 })
7137 .await
7138 .unwrap();
7139 }
7140 }
7141 });
7142
7143 let leecher_storage = make_storage(&data, piece_length);
7145 let leecher_meta = make_test_torrent(&data, piece_length);
7146
7147 let leecher_config = test_config();
7148 let (latx, lamask) = test_alert_channel();
7149 let (ldh, ldm, _ldj) = test_register_disk(leecher_meta.info_hash, leecher_storage).await;
7150 let leecher = TorrentHandle::from_torrent(
7151 leecher_meta,
7152 irontide_core::TorrentVersion::V1Only,
7153 None,
7154 ldh,
7155 ldm,
7156 leecher_config,
7157 test_dht_rx(),
7158 test_dht_rx(),
7159 None,
7160 None,
7161 crate::slot_tuner::SlotTuner::disabled(4),
7162 latx,
7163 lamask,
7164 None,
7165 None,
7166 test_ban_manager(),
7167 test_ip_filter(),
7168 Arc::new(Vec::new()),
7169 None,
7170 None,
7171 Arc::new(crate::transport::NetworkFactory::tokio()),
7172 None, Arc::new(crate::stats::SessionCounters::new()),
7174 )
7175 .await
7176 .unwrap();
7177
7178 leecher
7180 .add_peers(vec![seeder_addr], PeerSource::Tracker)
7181 .await
7182 .unwrap();
7183
7184 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
7188 loop {
7189 tokio::time::sleep(Duration::from_millis(200)).await;
7190 let stats = leecher.stats().await.unwrap();
7191 if stats.state == TorrentState::Seeding {
7192 assert_eq!(stats.pieces_have, 2);
7193 assert_eq!(stats.pieces_total, 2);
7194 break;
7195 }
7196 if tokio::time::Instant::now() > deadline {
7197 let stats = leecher.stats().await.unwrap();
7198 panic!(
7199 "seeder/leecher: leecher did not complete, state={:?}, have={}/{}, connected={}, available={}",
7200 stats.state,
7201 stats.pieces_have,
7202 stats.pieces_total,
7203 stats.peers_connected,
7204 stats.peers_available,
7205 );
7206 }
7207 }
7208
7209 leecher.shutdown().await.unwrap();
7210 seeder_task.abort();
7211 }
7212
7213 #[tokio::test]
7216 async fn magnet_initial_stats() {
7217 let magnet = Magnet {
7218 info_hashes: irontide_core::InfoHashes::v1_only(
7219 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
7220 ),
7221 display_name: Some("magnet test".into()),
7222 trackers: vec![],
7223 peers: vec![],
7224 selected_files: None,
7225 };
7226
7227 let (atx, amask) = test_alert_channel();
7228 let (dm, _dj) = test_disk_manager();
7229 let handle = TorrentHandle::from_magnet(
7230 magnet,
7231 dm,
7232 test_config(),
7233 test_dht_rx(),
7234 test_dht_rx(),
7235 None,
7236 None,
7237 crate::slot_tuner::SlotTuner::disabled(4),
7238 atx,
7239 amask,
7240 None,
7241 None,
7242 test_ban_manager(),
7243 test_ip_filter(),
7244 Arc::new(Vec::new()),
7245 None,
7246 None,
7247 Arc::new(crate::transport::NetworkFactory::tokio()),
7248 None, Arc::new(crate::stats::SessionCounters::new()),
7250 )
7251 .await
7252 .unwrap();
7253
7254 let stats = handle.stats().await.unwrap();
7255 assert_eq!(stats.state, TorrentState::FetchingMetadata);
7256 assert_eq!(stats.pieces_total, 0);
7257 assert_eq!(stats.pieces_have, 0);
7258 assert_eq!(stats.downloaded, 0);
7259 assert_eq!(stats.uploaded, 0);
7260 assert_eq!(stats.peers_connected, 0);
7261 assert_eq!(stats.peers_available, 0);
7262
7263 handle.shutdown().await.unwrap();
7264 }
7265
7266 #[tokio::test]
7269 async fn tracker_populated_from_metadata() {
7270 use serde::Serialize;
7271
7272 #[derive(Serialize)]
7273 struct Info<'a> {
7274 length: u64,
7275 name: &'a str,
7276 #[serde(rename = "piece length")]
7277 piece_length: u64,
7278 #[serde(with = "serde_bytes")]
7279 pieces: &'a [u8],
7280 }
7281
7282 #[derive(Serialize)]
7283 struct Torrent<'a> {
7284 announce: &'a str,
7285 info: Info<'a>,
7286 }
7287
7288 let data = vec![0xAB; 16384];
7289 let hash = irontide_core::sha1(&data);
7290 let mut pieces = Vec::new();
7291 pieces.extend_from_slice(hash.as_bytes());
7292
7293 let t = Torrent {
7294 announce: "http://tracker.example.com:8080/announce",
7295 info: Info {
7296 length: data.len() as u64,
7297 name: "test",
7298 piece_length: 16384,
7299 pieces: &pieces,
7300 },
7301 };
7302
7303 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7304 let meta = torrent_from_bytes(&bytes).unwrap();
7305 assert!(meta.announce.is_some());
7306
7307 let storage = make_storage(&data, 16384);
7308 let config = test_config();
7309
7310 let (atx, amask) = test_alert_channel();
7313 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7314 let handle = TorrentHandle::from_torrent(
7315 meta,
7316 irontide_core::TorrentVersion::V1Only,
7317 None,
7318 dh,
7319 dm,
7320 config,
7321 test_dht_rx(),
7322 test_dht_rx(),
7323 None,
7324 None,
7325 crate::slot_tuner::SlotTuner::disabled(4),
7326 atx,
7327 amask,
7328 None,
7329 None,
7330 test_ban_manager(),
7331 test_ip_filter(),
7332 Arc::new(Vec::new()),
7333 None,
7334 None,
7335 Arc::new(crate::transport::NetworkFactory::tokio()),
7336 None, Arc::new(crate::stats::SessionCounters::new()),
7338 )
7339 .await
7340 .unwrap();
7341
7342 let stats = handle.stats().await.unwrap();
7343 assert_eq!(stats.state, TorrentState::Downloading);
7344
7345 handle.shutdown().await.unwrap();
7346 }
7347
7348 #[tokio::test]
7351 async fn private_torrent_no_dht_field() {
7352 use serde::Serialize;
7353
7354 #[derive(Serialize)]
7355 struct Info<'a> {
7356 length: u64,
7357 name: &'a str,
7358 #[serde(rename = "piece length")]
7359 piece_length: u64,
7360 #[serde(with = "serde_bytes")]
7361 pieces: &'a [u8],
7362 private: i64,
7363 }
7364
7365 #[derive(Serialize)]
7366 struct Torrent<'a> {
7367 announce: &'a str,
7368 info: Info<'a>,
7369 }
7370
7371 let data = vec![0xAB; 16384];
7372 let hash = irontide_core::sha1(&data);
7373 let mut pieces = Vec::new();
7374 pieces.extend_from_slice(hash.as_bytes());
7375
7376 let t = Torrent {
7377 announce: "http://private-tracker.example.com/announce",
7378 info: Info {
7379 length: data.len() as u64,
7380 name: "private_test",
7381 piece_length: 16384,
7382 pieces: &pieces,
7383 private: 1,
7384 },
7385 };
7386
7387 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7388 let meta = torrent_from_bytes(&bytes).unwrap();
7389 assert_eq!(meta.info.private, Some(1));
7390
7391 let storage = make_storage(&data, 16384);
7392 let config = test_config();
7393
7394 let (atx, amask) = test_alert_channel();
7395 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7396 let handle = TorrentHandle::from_torrent(
7397 meta,
7398 irontide_core::TorrentVersion::V1Only,
7399 None,
7400 dh,
7401 dm,
7402 config,
7403 test_dht_rx(),
7404 test_dht_rx(),
7405 None,
7406 None,
7407 crate::slot_tuner::SlotTuner::disabled(4),
7408 atx,
7409 amask,
7410 None,
7411 None,
7412 test_ban_manager(),
7413 test_ip_filter(),
7414 Arc::new(Vec::new()),
7415 None,
7416 None,
7417 Arc::new(crate::transport::NetworkFactory::tokio()),
7418 None, Arc::new(crate::stats::SessionCounters::new()),
7420 )
7421 .await
7422 .unwrap();
7423
7424 let stats = handle.stats().await.unwrap();
7425 assert_eq!(stats.state, TorrentState::Downloading);
7426
7427 handle.shutdown().await.unwrap();
7428 }
7429
7430 #[tokio::test]
7433 async fn magnet_no_tracker_before_metadata() {
7434 let magnet = Magnet {
7435 info_hashes: irontide_core::InfoHashes::v1_only(
7436 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
7437 ),
7438 display_name: Some("magnet test".into()),
7439 trackers: vec![],
7440 peers: vec![],
7441 selected_files: None,
7442 };
7443
7444 let (atx, amask) = test_alert_channel();
7445 let (dm, _dj) = test_disk_manager();
7446 let handle = TorrentHandle::from_magnet(
7447 magnet,
7448 dm,
7449 test_config(),
7450 test_dht_rx(),
7451 test_dht_rx(),
7452 None,
7453 None,
7454 crate::slot_tuner::SlotTuner::disabled(4),
7455 atx,
7456 amask,
7457 None,
7458 None,
7459 test_ban_manager(),
7460 test_ip_filter(),
7461 Arc::new(Vec::new()),
7462 None,
7463 None,
7464 Arc::new(crate::transport::NetworkFactory::tokio()),
7465 None, Arc::new(crate::stats::SessionCounters::new()),
7467 )
7468 .await
7469 .unwrap();
7470
7471 let stats = handle.stats().await.unwrap();
7472 assert_eq!(stats.state, TorrentState::FetchingMetadata);
7473
7474 tokio::time::sleep(Duration::from_millis(50)).await;
7478
7479 handle.shutdown().await.unwrap();
7480 }
7481
7482 #[tokio::test]
7485 async fn pause_and_resume() {
7486 let data = vec![0xEEu8; 32768];
7487 let meta = make_test_torrent(&data, 16384);
7488 let storage = make_storage(&data, 16384);
7489 let config = test_config();
7490 let (atx, amask) = test_alert_channel();
7491 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7492 let handle = TorrentHandle::from_torrent(
7493 meta,
7494 irontide_core::TorrentVersion::V1Only,
7495 None,
7496 dh,
7497 dm,
7498 config,
7499 test_dht_rx(),
7500 test_dht_rx(),
7501 None,
7502 None,
7503 crate::slot_tuner::SlotTuner::disabled(4),
7504 atx,
7505 amask,
7506 None,
7507 None,
7508 test_ban_manager(),
7509 test_ip_filter(),
7510 Arc::new(Vec::new()),
7511 None,
7512 None,
7513 Arc::new(crate::transport::NetworkFactory::tokio()),
7514 None, Arc::new(crate::stats::SessionCounters::new()),
7516 )
7517 .await
7518 .unwrap();
7519
7520 let stats = handle.stats().await.unwrap();
7521 assert_eq!(stats.state, TorrentState::Downloading);
7522
7523 handle.pause().await.unwrap();
7524 tokio::time::sleep(Duration::from_millis(50)).await;
7525 let stats = handle.stats().await.unwrap();
7526 assert_eq!(stats.state, TorrentState::Paused);
7527
7528 handle.resume().await.unwrap();
7529 tokio::time::sleep(Duration::from_millis(50)).await;
7530 let stats = handle.stats().await.unwrap();
7531 assert_eq!(stats.state, TorrentState::Downloading);
7532
7533 handle.shutdown().await.unwrap();
7534 }
7535
7536 #[tokio::test]
7539 async fn pause_already_paused_is_noop() {
7540 let data = vec![0xEEu8; 32768];
7541 let meta = make_test_torrent(&data, 16384);
7542 let storage = make_storage(&data, 16384);
7543 let config = test_config();
7544 let (atx, amask) = test_alert_channel();
7545 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7546 let handle = TorrentHandle::from_torrent(
7547 meta,
7548 irontide_core::TorrentVersion::V1Only,
7549 None,
7550 dh,
7551 dm,
7552 config,
7553 test_dht_rx(),
7554 test_dht_rx(),
7555 None,
7556 None,
7557 crate::slot_tuner::SlotTuner::disabled(4),
7558 atx,
7559 amask,
7560 None,
7561 None,
7562 test_ban_manager(),
7563 test_ip_filter(),
7564 Arc::new(Vec::new()),
7565 None,
7566 None,
7567 Arc::new(crate::transport::NetworkFactory::tokio()),
7568 None, Arc::new(crate::stats::SessionCounters::new()),
7570 )
7571 .await
7572 .unwrap();
7573
7574 handle.pause().await.unwrap();
7575 tokio::time::sleep(Duration::from_millis(50)).await;
7576 handle.pause().await.unwrap(); tokio::time::sleep(Duration::from_millis(50)).await;
7578 let stats = handle.stats().await.unwrap();
7579 assert_eq!(stats.state, TorrentState::Paused);
7580
7581 handle.shutdown().await.unwrap();
7582 }
7583
7584 #[tokio::test]
7590 async fn incoming_request_served_from_storage() {
7591 let data = vec![0xABu8; 16384];
7592 let meta = make_test_torrent(&data, 16384);
7593 let info_hash = meta.info_hash;
7594 let storage = make_storage(&data, 16384);
7595
7596 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7597 let listen_addr = listener.local_addr().unwrap();
7598 drop(listener);
7599
7600 let config = TorrentConfig {
7601 listen_port: listen_addr.port(),
7602 ..test_config()
7603 };
7604
7605 let (atx, amask) = test_alert_channel();
7606 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7607 let handle = TorrentHandle::from_torrent(
7608 meta,
7609 irontide_core::TorrentVersion::V1Only,
7610 None,
7611 dh,
7612 dm,
7613 config,
7614 test_dht_rx(),
7615 test_dht_rx(),
7616 None,
7617 None,
7618 crate::slot_tuner::SlotTuner::disabled(4),
7619 atx,
7620 amask,
7621 None,
7622 None,
7623 test_ban_manager(),
7624 test_ip_filter(),
7625 Arc::new(Vec::new()),
7626 None,
7627 None,
7628 Arc::new(crate::transport::NetworkFactory::tokio()),
7629 None, Arc::new(crate::stats::SessionCounters::new()),
7631 )
7632 .await
7633 .unwrap();
7634
7635 tokio::time::sleep(Duration::from_millis(50)).await;
7636
7637 let seed_data = data.clone();
7639 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7640 let seeder_task = tokio::spawn(async move {
7641 let (reader, writer) = tokio::io::split(seed_stream);
7642 let mut writer = writer;
7643 let mut reader = reader;
7644
7645 let hs = Handshake::new(
7646 info_hash,
7647 Id20::from_hex("6666666666666666666666666666666666666666").unwrap(),
7648 );
7649 writer.write_all(&hs.to_bytes()).await.unwrap();
7650 writer.flush().await.unwrap();
7651 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7652 reader.read_exact(&mut hs_buf).await.unwrap();
7653
7654 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7655 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7656
7657 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
7659 let payload = ext_hs.to_bytes().unwrap();
7660 framed_write
7661 .send(Message::Extended { ext_id: 0, payload })
7662 .await
7663 .unwrap();
7664
7665 let mut bf = Bitfield::new(1);
7667 bf.set(0);
7668 framed_write
7669 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7670 .await
7671 .unwrap();
7672 framed_write.send(Message::Unchoke).await.unwrap();
7673
7674 while let Some(Ok(msg)) = framed_read.next().await {
7676 if let Message::Request {
7677 index,
7678 begin,
7679 length,
7680 } = msg
7681 {
7682 let start = begin as usize;
7683 let end = start + length as usize;
7684 framed_write
7685 .send(Message::Piece {
7686 index,
7687 begin,
7688 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
7689 data_1: Bytes::new(),
7690 })
7691 .await
7692 .unwrap();
7693 }
7694 }
7695 });
7696
7697 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7699 loop {
7700 tokio::time::sleep(Duration::from_millis(100)).await;
7701 let stats = handle.stats().await.unwrap();
7702 if stats.pieces_have == 1 {
7703 break;
7704 }
7705 assert!(
7706 tokio::time::Instant::now() <= deadline,
7707 "piece download did not complete within 5s"
7708 );
7709 }
7710
7711 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7713 let expected_data = data.clone();
7714 let leecher_task = tokio::spawn(async move {
7715 let (reader, writer) = tokio::io::split(leech_stream);
7716 let mut writer = writer;
7717 let mut reader = reader;
7718
7719 let hs = Handshake::new(
7720 info_hash,
7721 Id20::from_hex("7777777777777777777777777777777777777777").unwrap(),
7722 );
7723 writer.write_all(&hs.to_bytes()).await.unwrap();
7724 writer.flush().await.unwrap();
7725 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7726 reader.read_exact(&mut hs_buf).await.unwrap();
7727
7728 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7729 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7730
7731 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
7733 let payload = ext_hs.to_bytes().unwrap();
7734 framed_write
7735 .send(Message::Extended { ext_id: 0, payload })
7736 .await
7737 .unwrap();
7738
7739 framed_write.send(Message::Interested).await.unwrap();
7741
7742 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
7743 loop {
7744 tokio::select! {
7745 msg = framed_read.next() => {
7746 match msg {
7747 Some(Ok(Message::Unchoke)) => { break; }
7748 Some(Ok(_)) => {}
7749 _ => panic!("connection closed before unchoke"),
7750 }
7751 }
7752 () = tokio::time::sleep_until(deadline) => {
7753 panic!("timed out waiting for unchoke");
7754 }
7755 }
7756 }
7757
7758 framed_write
7760 .send(Message::Request {
7761 index: 0,
7762 begin: 0,
7763 length: 16384,
7764 })
7765 .await
7766 .unwrap();
7767
7768 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7770 loop {
7771 tokio::select! {
7772 msg = framed_read.next() => {
7773 match msg {
7774 Some(Ok(Message::Piece { index, begin, data_0, data_1 })) => {
7775 assert_eq!(index, 0);
7776 assert_eq!(begin, 0);
7777 let _ = &data_1; assert_eq!(data_0.as_ref(), expected_data.as_slice());
7779 return; }
7781 Some(Ok(_)) => {}
7782 Some(Err(e)) => panic!("error reading: {e}"),
7783 None => panic!("connection closed before piece"),
7784 }
7785 }
7786 () = tokio::time::sleep_until(deadline) => {
7787 panic!("timed out waiting for piece data");
7788 }
7789 }
7790 }
7791 });
7792
7793 let result = tokio::time::timeout(Duration::from_secs(20), leecher_task).await;
7795 match result {
7796 Ok(Ok(())) => {}
7797 Ok(Err(e)) => panic!("leecher task panicked: {e}"),
7798 Err(elapsed) => panic!("test timed out after {elapsed}"),
7799 }
7800
7801 let stats = handle.stats().await.unwrap();
7803 assert!(
7804 stats.uploaded > 0,
7805 "expected uploaded > 0, got {}",
7806 stats.uploaded
7807 );
7808
7809 handle.shutdown().await.unwrap();
7810 seeder_task.abort();
7811 }
7812
7813 #[tokio::test]
7816 async fn seed_ratio_limit_stops_torrent() {
7817 let data = vec![0xCCu8; 16384];
7820 let meta = make_test_torrent(&data, 16384);
7821 let info_hash = meta.info_hash;
7822 let storage = make_storage(&data, 16384);
7823
7824 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7825 let listen_addr = listener.local_addr().unwrap();
7826 drop(listener);
7827
7828 let config = TorrentConfig {
7829 listen_port: listen_addr.port(),
7830 seed_ratio_limit: Some(1.0),
7831 ..test_config()
7832 };
7833
7834 let (atx, amask) = test_alert_channel();
7835 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7836 let handle = TorrentHandle::from_torrent(
7837 meta,
7838 irontide_core::TorrentVersion::V1Only,
7839 None,
7840 dh,
7841 dm,
7842 config,
7843 test_dht_rx(),
7844 test_dht_rx(),
7845 None,
7846 None,
7847 crate::slot_tuner::SlotTuner::disabled(4),
7848 atx,
7849 amask,
7850 None,
7851 None,
7852 test_ban_manager(),
7853 test_ip_filter(),
7854 Arc::new(Vec::new()),
7855 None,
7856 None,
7857 Arc::new(crate::transport::NetworkFactory::tokio()),
7858 None, Arc::new(crate::stats::SessionCounters::new()),
7860 )
7861 .await
7862 .unwrap();
7863
7864 tokio::time::sleep(Duration::from_millis(50)).await;
7865
7866 let seed_data = data.clone();
7868 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7869 let seeder_task = tokio::spawn(async move {
7870 let (reader, writer) = tokio::io::split(seed_stream);
7871 let mut writer = writer;
7872 let mut reader = reader;
7873
7874 let hs = Handshake::new(
7875 info_hash,
7876 Id20::from_hex("8888888888888888888888888888888888888888").unwrap(),
7877 );
7878 writer.write_all(&hs.to_bytes()).await.unwrap();
7879 writer.flush().await.unwrap();
7880 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7881 reader.read_exact(&mut hs_buf).await.unwrap();
7882
7883 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7884 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7885
7886 let _msg = framed_read.next().await;
7887 let ext_hs = ExtHandshake::new();
7888 let payload = ext_hs.to_bytes().unwrap();
7889 framed_write
7890 .send(Message::Extended { ext_id: 0, payload })
7891 .await
7892 .unwrap();
7893
7894 let mut bf = Bitfield::new(1);
7895 bf.set(0);
7896 framed_write
7897 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7898 .await
7899 .unwrap();
7900 framed_write.send(Message::Unchoke).await.unwrap();
7901
7902 while let Some(Ok(msg)) = framed_read.next().await {
7903 if let Message::Request {
7904 index,
7905 begin,
7906 length,
7907 } = msg
7908 {
7909 let start = begin as usize;
7910 let end = start + length as usize;
7911 framed_write
7912 .send(Message::Piece {
7913 index,
7914 begin,
7915 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
7916 data_1: Bytes::new(),
7917 })
7918 .await
7919 .unwrap();
7920 }
7921 }
7922 });
7923
7924 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7926 loop {
7927 tokio::time::sleep(Duration::from_millis(100)).await;
7928 let stats = handle.stats().await.unwrap();
7929 if stats.state == TorrentState::Seeding {
7930 break;
7931 }
7932 assert!(
7933 tokio::time::Instant::now() <= deadline,
7934 "download did not complete within 5s"
7935 );
7936 }
7937
7938 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7940 let leecher_task = tokio::spawn(async move {
7941 let (reader, writer) = tokio::io::split(leech_stream);
7942 let mut writer = writer;
7943 let mut reader = reader;
7944
7945 let hs = Handshake::new(
7946 info_hash,
7947 Id20::from_hex("9999999999999999999999999999999999999999").unwrap(),
7948 );
7949 writer.write_all(&hs.to_bytes()).await.unwrap();
7950 writer.flush().await.unwrap();
7951 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7952 reader.read_exact(&mut hs_buf).await.unwrap();
7953
7954 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7955 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7956
7957 let _msg = framed_read.next().await;
7958 let ext_hs = ExtHandshake::new();
7959 let payload = ext_hs.to_bytes().unwrap();
7960 framed_write
7961 .send(Message::Extended { ext_id: 0, payload })
7962 .await
7963 .unwrap();
7964
7965 framed_write.send(Message::Interested).await.unwrap();
7966
7967 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
7969 loop {
7970 tokio::select! {
7971 msg = framed_read.next() => {
7972 match msg {
7973 Some(Ok(Message::Unchoke)) => break,
7974 Some(Ok(_)) => {}
7975 _ => return, }
7977 }
7978 () = tokio::time::sleep_until(deadline) => return,
7979 }
7980 }
7981
7982 framed_write
7984 .send(Message::Request {
7985 index: 0,
7986 begin: 0,
7987 length: 16384,
7988 })
7989 .await
7990 .unwrap();
7991
7992 while let Some(Ok(_msg)) = framed_read.next().await {}
7994 });
7995
7996 let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
7998 loop {
7999 tokio::time::sleep(Duration::from_millis(100)).await;
8000 let stats = handle.stats().await.unwrap();
8001 if stats.state == TorrentState::Stopped {
8002 assert!(
8003 stats.uploaded >= 16384,
8004 "expected uploaded >= 16384, got {}",
8005 stats.uploaded
8006 );
8007 break;
8008 }
8009 if tokio::time::Instant::now() > deadline {
8010 let stats = handle.stats().await.unwrap();
8011 panic!(
8012 "expected Stopped, got {:?}, uploaded={}, downloaded={}",
8013 stats.state, stats.uploaded, stats.downloaded
8014 );
8015 }
8016 }
8017
8018 handle.shutdown().await.unwrap();
8019 seeder_task.abort();
8020 leecher_task.abort();
8021 }
8022
8023 #[tokio::test]
8026 async fn resume_with_seeded_storage() {
8027 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
8029 let storage = make_seeded_storage(&data, 16384);
8030 let config = test_config();
8031
8032 let (atx, amask) = test_alert_channel();
8033 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8034 let handle = TorrentHandle::from_torrent(
8035 meta,
8036 irontide_core::TorrentVersion::V1Only,
8037 None,
8038 dh,
8039 dm,
8040 config,
8041 test_dht_rx(),
8042 test_dht_rx(),
8043 None,
8044 None,
8045 crate::slot_tuner::SlotTuner::disabled(4),
8046 atx,
8047 amask,
8048 None,
8049 None,
8050 test_ban_manager(),
8051 test_ip_filter(),
8052 Arc::new(Vec::new()),
8053 None,
8054 None,
8055 Arc::new(crate::transport::NetworkFactory::tokio()),
8056 None, Arc::new(crate::stats::SessionCounters::new()),
8058 )
8059 .await
8060 .unwrap();
8061
8062 tokio::time::sleep(Duration::from_millis(100)).await;
8064
8065 let stats = handle.stats().await.unwrap();
8066 assert_eq!(
8067 stats.state,
8068 TorrentState::Seeding,
8069 "should start as seeder with all pieces verified"
8070 );
8071 assert_eq!(stats.pieces_have, 2);
8072 assert_eq!(stats.pieces_total, 2);
8073
8074 handle.shutdown().await.unwrap();
8075 }
8076
8077 #[tokio::test]
8080 async fn save_resume_data_captures_state() {
8081 let data = vec![0xAB; 32768];
8082 let meta = make_test_torrent(&data, 16384);
8083 let info_hash = meta.info_hash;
8084 let storage = make_storage(&data, 16384);
8085 let config = test_config();
8086
8087 let (atx, amask) = test_alert_channel();
8088 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8089 let handle = TorrentHandle::from_torrent(
8090 meta,
8091 irontide_core::TorrentVersion::V1Only,
8092 None,
8093 dh,
8094 dm,
8095 config,
8096 test_dht_rx(),
8097 test_dht_rx(),
8098 None,
8099 None,
8100 crate::slot_tuner::SlotTuner::disabled(4),
8101 atx,
8102 amask,
8103 None,
8104 None,
8105 test_ban_manager(),
8106 test_ip_filter(),
8107 Arc::new(Vec::new()),
8108 None,
8109 None,
8110 Arc::new(crate::transport::NetworkFactory::tokio()),
8111 None, Arc::new(crate::stats::SessionCounters::new()),
8113 )
8114 .await
8115 .unwrap();
8116
8117 tokio::time::sleep(Duration::from_millis(50)).await;
8119
8120 let rd = handle.save_resume_data().await.unwrap();
8121
8122 assert_eq!(rd.file_format, "libtorrent resume file");
8123 assert_eq!(rd.file_version, 1);
8124 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
8125 assert_eq!(rd.name, "test");
8126 assert_eq!(rd.save_path, "/tmp");
8127 assert_eq!(rd.paused, 0);
8128 assert!(!rd.pieces.is_empty());
8130 assert_eq!(rd.total_uploaded, 0);
8132 assert_eq!(rd.total_downloaded, 0);
8133
8134 handle.shutdown().await.unwrap();
8135 }
8136
8137 #[tokio::test]
8140 async fn save_resume_data_seeder() {
8141 let data = vec![0xCD; 32768];
8142 let meta = make_test_torrent(&data, 16384);
8143 let info_hash = meta.info_hash;
8144 let storage = make_seeded_storage(&data, 16384);
8145 let config = test_config();
8146
8147 let (atx, amask) = test_alert_channel();
8148 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8149 let handle = TorrentHandle::from_torrent(
8150 meta,
8151 irontide_core::TorrentVersion::V1Only,
8152 None,
8153 dh,
8154 dm,
8155 config,
8156 test_dht_rx(),
8157 test_dht_rx(),
8158 None,
8159 None,
8160 crate::slot_tuner::SlotTuner::disabled(4),
8161 atx,
8162 amask,
8163 None,
8164 None,
8165 test_ban_manager(),
8166 test_ip_filter(),
8167 Arc::new(Vec::new()),
8168 None,
8169 None,
8170 Arc::new(crate::transport::NetworkFactory::tokio()),
8171 None, Arc::new(crate::stats::SessionCounters::new()),
8173 )
8174 .await
8175 .unwrap();
8176
8177 tokio::time::sleep(Duration::from_millis(100)).await;
8179
8180 let rd = handle.save_resume_data().await.unwrap();
8181
8182 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
8183 assert_eq!(rd.name, "test");
8184 assert_eq!(rd.seed_mode, 1, "seeder should have seed_mode=1");
8185 assert_eq!(rd.paused, 0);
8186 assert_eq!(rd.pieces.len(), 1);
8189 assert_eq!(
8190 rd.pieces[0] & 0xC0,
8191 0xC0,
8192 "both pieces should be marked complete"
8193 );
8194
8195 handle.shutdown().await.unwrap();
8196 }
8197
8198 #[tokio::test]
8201 async fn save_resume_data_paused() {
8202 let data = vec![0xEF; 16384];
8203 let meta = make_test_torrent(&data, 16384);
8204 let storage = make_storage(&data, 16384);
8205 let config = test_config();
8206
8207 let (atx, amask) = test_alert_channel();
8208 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8209 let handle = TorrentHandle::from_torrent(
8210 meta,
8211 irontide_core::TorrentVersion::V1Only,
8212 None,
8213 dh,
8214 dm,
8215 config,
8216 test_dht_rx(),
8217 test_dht_rx(),
8218 None,
8219 None,
8220 crate::slot_tuner::SlotTuner::disabled(4),
8221 atx,
8222 amask,
8223 None,
8224 None,
8225 test_ban_manager(),
8226 test_ip_filter(),
8227 Arc::new(Vec::new()),
8228 None,
8229 None,
8230 Arc::new(crate::transport::NetworkFactory::tokio()),
8231 None, Arc::new(crate::stats::SessionCounters::new()),
8233 )
8234 .await
8235 .unwrap();
8236
8237 tokio::time::sleep(Duration::from_millis(50)).await;
8238 handle.pause().await.unwrap();
8239 tokio::time::sleep(Duration::from_millis(50)).await;
8240
8241 let rd = handle.save_resume_data().await.unwrap();
8242 assert_eq!(rd.paused, 1, "paused torrent should have paused=1");
8243 assert_eq!(rd.seed_mode, 0);
8244
8245 handle.shutdown().await.unwrap();
8246 }
8247
8248 #[tokio::test]
8251 async fn set_file_priority_and_read_back() {
8252 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
8253 let mut torrent_bytes = b"d4:info".to_vec();
8254 torrent_bytes.extend_from_slice(info_bytes);
8255 torrent_bytes.push(b'e');
8256
8257 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
8258 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
8259 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8260 let config = TorrentConfig {
8261 listen_port: 0,
8262 ..Default::default()
8263 };
8264
8265 let (atx, amask) = test_alert_channel();
8266 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8267 let handle = TorrentHandle::from_torrent(
8268 meta,
8269 irontide_core::TorrentVersion::V1Only,
8270 None,
8271 dh,
8272 dm,
8273 config,
8274 test_dht_rx(),
8275 test_dht_rx(),
8276 None,
8277 None,
8278 crate::slot_tuner::SlotTuner::disabled(4),
8279 atx,
8280 amask,
8281 None,
8282 None,
8283 test_ban_manager(),
8284 test_ip_filter(),
8285 Arc::new(Vec::new()),
8286 None,
8287 None,
8288 Arc::new(crate::transport::NetworkFactory::tokio()),
8289 None, Arc::new(crate::stats::SessionCounters::new()),
8291 )
8292 .await
8293 .unwrap();
8294
8295 let prios = handle.file_priorities().await.unwrap();
8297 assert_eq!(prios.len(), 2);
8298 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
8299
8300 handle
8302 .set_file_priority(0, FilePriority::Skip)
8303 .await
8304 .unwrap();
8305
8306 let prios = handle.file_priorities().await.unwrap();
8307 assert_eq!(prios[0], FilePriority::Skip);
8308 assert_eq!(prios[1], FilePriority::Normal);
8309
8310 let result = handle.set_file_priority(99, FilePriority::High).await;
8312 assert!(result.is_err());
8313
8314 handle.shutdown().await.unwrap();
8315 tokio::time::sleep(Duration::from_millis(50)).await;
8316 }
8317
8318 async fn spawn_test_torrent_multifile() -> TorrentHandle {
8322 let meta = make_multi_file_meta(&[(100, "a.bin"), (150, "b.bin"), (100, "c.bin")], 100);
8323 let lengths = Lengths::new(350, 100, DEFAULT_CHUNK_SIZE);
8324 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8325 let config = TorrentConfig {
8326 listen_port: 0,
8327 ..Default::default()
8328 };
8329 let (atx, amask) = test_alert_channel();
8330 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8331 TorrentHandle::from_torrent(
8332 meta,
8333 irontide_core::TorrentVersion::V1Only,
8334 None,
8335 dh,
8336 dm,
8337 config,
8338 test_dht_rx(),
8339 test_dht_rx(),
8340 None,
8341 None,
8342 crate::slot_tuner::SlotTuner::disabled(4),
8343 atx,
8344 amask,
8345 None,
8346 None,
8347 test_ban_manager(),
8348 test_ip_filter(),
8349 Arc::new(Vec::new()),
8350 None,
8351 None,
8352 Arc::new(crate::transport::NetworkFactory::tokio()),
8353 None,
8354 Arc::new(crate::stats::SessionCounters::new()),
8355 )
8356 .await
8357 .unwrap()
8358 }
8359
8360 #[tokio::test]
8366 async fn set_file_priority_updates_wanted_and_priorities() {
8367 let handle = spawn_test_torrent_multifile().await;
8368
8369 let prios = handle.file_priorities().await.unwrap();
8370 assert_eq!(prios.len(), 3);
8371 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
8372
8373 handle
8374 .set_file_priority(1, FilePriority::Skip)
8375 .await
8376 .unwrap();
8377 assert_eq!(
8378 handle.file_priorities().await.unwrap()[1],
8379 FilePriority::Skip
8380 );
8381
8382 handle
8383 .set_file_priority(1, FilePriority::Normal)
8384 .await
8385 .unwrap();
8386 assert_eq!(
8387 handle.file_priorities().await.unwrap()[1],
8388 FilePriority::Normal
8389 );
8390
8391 assert!(
8393 handle
8394 .set_file_priority(99, FilePriority::High)
8395 .await
8396 .is_err()
8397 );
8398
8399 handle.shutdown().await.unwrap();
8400 tokio::time::sleep(Duration::from_millis(50)).await;
8401 }
8402
8403 fn priority_test_actor(files: &[(u64, &str)], piece_length: u64) -> TorrentActor {
8409 use irontide_storage::Bitfield;
8410 let meta = make_multi_file_meta(files, piece_length);
8411 let total: u64 = files.iter().map(|(l, _)| *l).sum();
8412 let lengths = Lengths::new(total, piece_length, DEFAULT_CHUNK_SIZE);
8413 let num_pieces = lengths.num_pieces();
8414 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8415
8416 let mut actor = TorrentActor::for_throttle_test(num_pieces, 0);
8417 actor.file_priorities = vec![FilePriority::Normal; files.len()];
8418 actor.wanted_pieces = crate::piece_selector::build_wanted_pieces(
8419 &actor.file_priorities,
8420 &file_lengths,
8421 &lengths,
8422 );
8423 actor.cached_files = Some(build_cached_file_info(&meta, &lengths));
8424
8425 let we_have = Bitfield::new(num_pieces);
8426 actor.atomic_states = Some(Arc::new(crate::piece_reservation::AtomicPieceStates::new(
8427 num_pieces,
8428 &we_have,
8429 &actor.wanted_pieces,
8430 )));
8431 actor.piece_tracker = Some(crate::piece_reservation::PieceTracker::new(
8432 num_pieces,
8433 &we_have,
8434 &actor.wanted_pieces,
8435 ));
8436 actor.meta = Some(meta);
8437 actor.lengths = Some(lengths);
8438 actor
8439 }
8440
8441 #[tokio::test]
8446 async fn apply_file_priority_scoped_matches_full_rebuild() {
8447 let files: &[(u64, &str)] = &[(100, "a"), (150, "b"), (100, "c"), (250, "d")];
8448 let piece_length = 100;
8449 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8450 for skip_idx in 0..files.len() {
8451 let mut actor = priority_test_actor(files, piece_length);
8452 actor
8453 .apply_file_priority_scoped(skip_idx, FilePriority::Skip)
8454 .unwrap();
8455
8456 let mut ref_prios = vec![FilePriority::Normal; files.len()];
8457 ref_prios[skip_idx] = FilePriority::Skip;
8458 let reference = crate::piece_selector::build_wanted_pieces(
8459 &ref_prios,
8460 &file_lengths,
8461 actor.lengths.as_ref().unwrap(),
8462 );
8463 for p in 0..actor.num_pieces {
8464 assert_eq!(
8465 actor.wanted_pieces.get(p),
8466 reference.get(p),
8467 "piece {p} mismatch after scoped skip of file {skip_idx}"
8468 );
8469 }
8470 }
8471 }
8472
8473 #[tokio::test]
8477 async fn apply_file_priority_scoped_handles_sub_piece_files() {
8478 let files: &[(u64, &str)] = &[(30, "a"), (30, "b"), (40, "c"), (100, "d")];
8479 let mut actor = priority_test_actor(files, 100);
8480
8481 actor
8482 .apply_file_priority_scoped(0, FilePriority::Skip)
8483 .unwrap();
8484 assert!(
8485 actor.wanted_pieces.get(0),
8486 "piece 0 wanted: b,c still want it"
8487 );
8488 actor
8489 .apply_file_priority_scoped(1, FilePriority::Skip)
8490 .unwrap();
8491 actor
8492 .apply_file_priority_scoped(2, FilePriority::Skip)
8493 .unwrap();
8494 assert!(
8495 !actor.wanted_pieces.get(0),
8496 "piece 0 unwanted: a,b,c all skip"
8497 );
8498 assert!(
8499 actor.wanted_pieces.get(1),
8500 "piece 1 still wanted (d Normal)"
8501 );
8502
8503 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8504 let prios = vec![
8505 FilePriority::Skip,
8506 FilePriority::Skip,
8507 FilePriority::Skip,
8508 FilePriority::Normal,
8509 ];
8510 let reference = crate::piece_selector::build_wanted_pieces(
8511 &prios,
8512 &file_lengths,
8513 actor.lengths.as_ref().unwrap(),
8514 );
8515 for p in 0..actor.num_pieces {
8516 assert_eq!(
8517 actor.wanted_pieces.get(p),
8518 reference.get(p),
8519 "piece {p} mismatch"
8520 );
8521 }
8522 }
8523
8524 #[tokio::test]
8527 async fn apply_file_priority_scoped_zero_length_file_no_panic() {
8528 let files: &[(u64, &str)] = &[(100, "a"), (0, "empty"), (100, "c")];
8529 let mut actor = priority_test_actor(files, 100);
8530
8531 let r = actor
8532 .apply_file_priority_scoped(1, FilePriority::Skip)
8533 .unwrap();
8534 assert!(
8535 r.0 > r.1,
8536 "zero-length file yields an empty range, got {r:?}"
8537 );
8538
8539 actor
8540 .apply_file_priority_scoped(0, FilePriority::Skip)
8541 .unwrap();
8542
8543 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8544 let prios = vec![FilePriority::Skip, FilePriority::Skip, FilePriority::Normal];
8545 let reference = crate::piece_selector::build_wanted_pieces(
8546 &prios,
8547 &file_lengths,
8548 actor.lengths.as_ref().unwrap(),
8549 );
8550 for p in 0..actor.num_pieces {
8551 assert_eq!(
8552 actor.wanted_pieces.get(p),
8553 reference.get(p),
8554 "piece {p} mismatch"
8555 );
8556 }
8557 }
8558
8559 #[tokio::test]
8562 async fn sync_piece_states_for_range_only_touches_range() {
8563 let files: &[(u64, &str)] = &[(200, "a"), (200, "b"), (200, "c")];
8564 let mut actor = priority_test_actor(files, 100); let (first, last) = actor
8566 .apply_file_priority_scoped(1, FilePriority::Skip)
8567 .unwrap();
8568 assert_eq!((first, last), (2, 3));
8569 actor.sync_piece_states_for_range(first, last);
8570
8571 let atomic = actor.atomic_states.as_ref().unwrap();
8572 assert_eq!(
8573 atomic.get(2),
8574 crate::piece_reservation::PieceState::Unwanted
8575 );
8576 assert_eq!(
8577 atomic.get(3),
8578 crate::piece_reservation::PieceState::Unwanted
8579 );
8580 for p in [0u32, 1, 4, 5] {
8581 assert_eq!(
8582 atomic.get(p),
8583 crate::piece_reservation::PieceState::Available,
8584 "piece {p} outside the range must be untouched"
8585 );
8586 }
8587 }
8588
8589 #[tokio::test]
8595 async fn order_map_coalesces_and_gen_is_monotone() {
8596 let mut actor = priority_test_actor(&[(200, "a"), (200, "b"), (200, "c")], 100);
8598 let gen0 = actor.order_map_tx.borrow().generation;
8599
8600 actor
8603 .apply_file_priority_scoped(0, FilePriority::Skip)
8604 .unwrap();
8605 actor.order_map_dirty = true;
8606 actor
8607 .apply_file_priority_scoped(2, FilePriority::Skip)
8608 .unwrap();
8609 actor.order_map_dirty = true;
8610 assert_eq!(
8611 actor.order_map_tx.borrow().generation,
8612 gen0,
8613 "no order-map rebuild before the tick"
8614 );
8615
8616 actor.rebuild_order_map_now();
8618 assert_eq!(
8619 actor.order_map_tx.borrow().generation,
8620 gen0 + 1,
8621 "exactly one coalesced rebuild"
8622 );
8623 assert!(!actor.order_map_dirty, "dirty flag cleared after rebuild");
8624
8625 let map = actor.order_map_tx.borrow();
8628 for p in [0u32, 1, 4, 5] {
8629 assert!(
8630 !map.order.contains(&p),
8631 "skipped piece {p} must be absent from the order"
8632 );
8633 }
8634 for p in [2u32, 3] {
8635 assert!(
8636 map.order.contains(&p),
8637 "wanted piece {p} must be present in the order"
8638 );
8639 }
8640 }
8641
8642 #[tokio::test]
8646 async fn rebuild_order_map_now_clears_dirty_flag() {
8647 let mut actor = priority_test_actor(&[(200, "a"), (200, "b")], 100);
8648 actor.order_map_dirty = true;
8649 actor.rebuild_order_map_now();
8650 assert!(!actor.order_map_dirty);
8651 }
8652
8653 #[tokio::test]
8654 async fn resume_data_preserves_file_priorities() {
8655 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
8656 let mut torrent_bytes = b"d4:info".to_vec();
8657 torrent_bytes.extend_from_slice(info_bytes);
8658 torrent_bytes.push(b'e');
8659
8660 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
8661 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
8662 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8663 let config = TorrentConfig {
8664 listen_port: 0,
8665 ..Default::default()
8666 };
8667
8668 let (atx, amask) = test_alert_channel();
8669 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8670 let handle = TorrentHandle::from_torrent(
8671 meta,
8672 irontide_core::TorrentVersion::V1Only,
8673 None,
8674 dh,
8675 dm,
8676 config,
8677 test_dht_rx(),
8678 test_dht_rx(),
8679 None,
8680 None,
8681 crate::slot_tuner::SlotTuner::disabled(4),
8682 atx,
8683 amask,
8684 None,
8685 None,
8686 test_ban_manager(),
8687 test_ip_filter(),
8688 Arc::new(Vec::new()),
8689 None,
8690 None,
8691 Arc::new(crate::transport::NetworkFactory::tokio()),
8692 None, Arc::new(crate::stats::SessionCounters::new()),
8694 )
8695 .await
8696 .unwrap();
8697
8698 handle
8700 .set_file_priority(0, FilePriority::High)
8701 .await
8702 .unwrap();
8703 handle
8704 .set_file_priority(1, FilePriority::Skip)
8705 .await
8706 .unwrap();
8707
8708 let rd = handle.save_resume_data().await.unwrap();
8710 assert_eq!(rd.file_priority, vec![7, 0]); let encoded = irontide_bencode::to_bytes(&rd).unwrap();
8714 let decoded: irontide_core::FastResumeData =
8715 irontide_bencode::from_bytes(&encoded).unwrap();
8716 assert_eq!(decoded.file_priority, vec![7, 0]);
8717
8718 handle.shutdown().await.unwrap();
8719 tokio::time::sleep(Duration::from_millis(50)).await;
8720 }
8721
8722 #[tokio::test]
8725 async fn upload_rate_limiting_caps_throughput() {
8726 let data = vec![0xAB; 16384]; let meta = make_test_torrent(&data, 16384);
8732 let info_hash = meta.info_hash;
8733 let storage = make_seeded_storage(&data, 16384);
8734
8735 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8736 let listen_addr = listener.local_addr().unwrap();
8737
8738 let config = TorrentConfig {
8739 listen_port: listen_addr.port(),
8740 upload_rate_limit: 1024, ..test_config()
8742 };
8743
8744 drop(listener);
8745 let (atx, amask) = test_alert_channel();
8746 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8747 let handle = TorrentHandle::from_torrent(
8748 meta,
8749 irontide_core::TorrentVersion::V1Only,
8750 None,
8751 dh,
8752 dm,
8753 config,
8754 test_dht_rx(),
8755 test_dht_rx(),
8756 None,
8757 None,
8758 crate::slot_tuner::SlotTuner::disabled(4),
8759 atx,
8760 amask,
8761 None,
8762 None,
8763 test_ban_manager(),
8764 test_ip_filter(),
8765 Arc::new(Vec::new()),
8766 None,
8767 None,
8768 Arc::new(crate::transport::NetworkFactory::tokio()),
8769 None, Arc::new(crate::stats::SessionCounters::new()),
8771 )
8772 .await
8773 .unwrap();
8774
8775 tokio::time::sleep(Duration::from_millis(50)).await;
8776
8777 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8779 let (reader, writer) = tokio::io::split(stream);
8780 let mut writer = writer;
8781 let mut reader = reader;
8782
8783 let hs = Handshake::new(
8784 info_hash,
8785 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
8786 );
8787 writer.write_all(&hs.to_bytes()).await.unwrap();
8788 writer.flush().await.unwrap();
8789 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8790 reader.read_exact(&mut hs_buf).await.unwrap();
8791
8792 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8793 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8794
8795 let _msg = framed_read.next().await;
8797 let ext_hs = ExtHandshake::new();
8798 let payload = ext_hs.to_bytes().unwrap();
8799 framed_write
8800 .send(Message::Extended { ext_id: 0, payload })
8801 .await
8802 .unwrap();
8803
8804 let _bf_msg = framed_read.next().await;
8806
8807 framed_write.send(Message::Interested).await.unwrap();
8809
8810 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
8812 loop {
8813 tokio::select! {
8814 msg = framed_read.next() => {
8815 match msg {
8816 Some(Ok(Message::Unchoke)) => break,
8817 Some(Ok(_)) => {}
8818 _ => panic!("connection closed before unchoke"),
8819 }
8820 }
8821 () = tokio::time::sleep_until(deadline) => {
8822 panic!("timed out waiting for unchoke");
8823 }
8824 }
8825 }
8826
8827 framed_write
8829 .send(Message::Request {
8830 index: 0,
8831 begin: 0,
8832 length: 16384,
8833 })
8834 .await
8835 .unwrap();
8836
8837 let mut got_piece = false;
8841 if let Ok(true) = tokio::time::timeout(Duration::from_secs(2), async {
8842 loop {
8843 match framed_read.next().await {
8844 Some(Ok(Message::Piece { .. })) => return true,
8845 Some(Ok(_)) => {}
8846 _ => return false,
8847 }
8848 }
8849 })
8850 .await
8851 {
8852 got_piece = true;
8853 }
8854
8855 assert!(
8857 !got_piece,
8858 "piece should be delayed by rate limiter (1 KB/s for 16 KB chunk)"
8859 );
8860
8861 let stats = handle.stats().await.unwrap();
8863 assert_eq!(stats.uploaded, 0); handle.shutdown().await.unwrap();
8866 }
8867
8868 #[tokio::test]
8869 async fn unlimited_rate_has_no_effect() {
8870 let data = vec![0xAB; 32768];
8872 let meta = make_test_torrent(&data, 16384);
8873 let storage = make_storage(&data, 16384);
8874 let config = test_config();
8875
8876 assert_eq!(config.upload_rate_limit, 0);
8878 assert_eq!(config.download_rate_limit, 0);
8879
8880 let (atx, amask) = test_alert_channel();
8881 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8882 let handle = TorrentHandle::from_torrent(
8883 meta,
8884 irontide_core::TorrentVersion::V1Only,
8885 None,
8886 dh,
8887 dm,
8888 config,
8889 test_dht_rx(),
8890 test_dht_rx(),
8891 None,
8892 None,
8893 crate::slot_tuner::SlotTuner::disabled(4),
8894 atx,
8895 amask,
8896 None,
8897 None,
8898 test_ban_manager(),
8899 test_ip_filter(),
8900 Arc::new(Vec::new()),
8901 None,
8902 None,
8903 Arc::new(crate::transport::NetworkFactory::tokio()),
8904 None, Arc::new(crate::stats::SessionCounters::new()),
8906 )
8907 .await
8908 .unwrap();
8909
8910 let stats = handle.stats().await.unwrap();
8911 assert_eq!(stats.state, TorrentState::Downloading);
8912 assert_eq!(stats.pieces_total, 2);
8913
8914 handle.shutdown().await.unwrap();
8915 }
8916
8917 #[tokio::test]
8918 async fn download_rate_limiting_throttles_requests() {
8919 let data = vec![0xAB; 32768];
8922 let meta = make_test_torrent(&data, 16384);
8923 let info_hash = meta.info_hash;
8924 let storage = make_storage(&data, 16384);
8925
8926 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8927 let listen_addr = listener.local_addr().unwrap();
8928
8929 let config = TorrentConfig {
8930 listen_port: listen_addr.port(),
8931 download_rate_limit: 1024, ..test_config()
8933 };
8934
8935 drop(listener);
8936 let (atx, amask) = test_alert_channel();
8937 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8938 let handle = TorrentHandle::from_torrent(
8939 meta,
8940 irontide_core::TorrentVersion::V1Only,
8941 None,
8942 dh,
8943 dm,
8944 config,
8945 test_dht_rx(),
8946 test_dht_rx(),
8947 None,
8948 None,
8949 crate::slot_tuner::SlotTuner::disabled(4),
8950 atx,
8951 amask,
8952 None,
8953 None,
8954 test_ban_manager(),
8955 test_ip_filter(),
8956 Arc::new(Vec::new()),
8957 None,
8958 None,
8959 Arc::new(crate::transport::NetworkFactory::tokio()),
8960 None, Arc::new(crate::stats::SessionCounters::new()),
8962 )
8963 .await
8964 .unwrap();
8965
8966 tokio::time::sleep(Duration::from_millis(50)).await;
8967
8968 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8970 let (reader, writer) = tokio::io::split(stream);
8971 let mut writer = writer;
8972 let mut reader = reader;
8973
8974 let hs = Handshake::new(
8975 info_hash,
8976 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
8977 );
8978 writer.write_all(&hs.to_bytes()).await.unwrap();
8979 writer.flush().await.unwrap();
8980 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8981 reader.read_exact(&mut hs_buf).await.unwrap();
8982
8983 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8984 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8985
8986 let _msg = framed_read.next().await;
8988 let ext_hs = ExtHandshake::new();
8989 let payload = ext_hs.to_bytes().unwrap();
8990 framed_write
8991 .send(Message::Extended { ext_id: 0, payload })
8992 .await
8993 .unwrap();
8994
8995 let mut bf = Bitfield::new(2);
8997 bf.set(0);
8998 bf.set(1);
8999 framed_write
9000 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
9001 .await
9002 .unwrap();
9003
9004 framed_write.send(Message::Unchoke).await.unwrap();
9006
9007 let mut requests_received = 0u32;
9011 let deadline = tokio::time::Instant::now() + Duration::from_millis(500);
9012 loop {
9013 match tokio::time::timeout(
9014 deadline.saturating_duration_since(tokio::time::Instant::now()),
9015 framed_read.next(),
9016 )
9017 .await
9018 {
9019 Ok(Some(Ok(Message::Request { .. }))) => {
9020 requests_received += 1;
9021 }
9022 Ok(Some(Ok(_))) => {}
9023 _ => break,
9024 }
9025 }
9026
9027 let stats = handle.stats().await.unwrap();
9028 assert_eq!(stats.state, TorrentState::Downloading);
9029
9030 assert!(
9033 requests_received <= 2,
9034 "with 1 KB/s limit, should get very few requests, got {requests_received}"
9035 );
9036
9037 handle.shutdown().await.unwrap();
9038 }
9039
9040 #[test]
9043 fn piece_contributor_tracking() {
9044 use std::net::IpAddr;
9045 let mut contributors: HashMap<u32, HashSet<IpAddr>> = HashMap::new();
9046 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9047 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9048
9049 contributors.entry(0).or_default().insert(ip1);
9050 contributors.entry(0).or_default().insert(ip2);
9051 assert_eq!(contributors[&0].len(), 2);
9052 assert!(contributors[&0].contains(&ip1));
9053 assert!(contributors[&0].contains(&ip2));
9054
9055 contributors.remove(&0);
9057 assert!(!contributors.contains_key(&0));
9058 }
9059
9060 #[test]
9061 fn parole_enter_on_hash_failure() {
9062 use crate::ban::ParoleState;
9063 use std::net::IpAddr;
9064
9065 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9066 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9067 let contributors = vec![ip1, ip2];
9068
9069 let parole = ParoleState {
9071 original_contributors: contributors.into_iter().collect(),
9072 parole_peer: None,
9073 };
9074
9075 assert_eq!(parole.original_contributors.len(), 2);
9076 assert!(parole.original_contributors.contains(&ip1));
9077 assert!(parole.original_contributors.contains(&ip2));
9078 assert!(parole.parole_peer.is_none());
9079 }
9080
9081 #[test]
9082 fn parole_success_strikes_originals() {
9083 use crate::ban::{BanConfig, BanManager, ParoleState};
9084 use std::net::IpAddr;
9085
9086 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9087 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9088 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
9089
9090 let mut mgr = BanManager::new(BanConfig {
9091 max_failures: 2,
9092 use_parole: true,
9093 });
9094
9095 let parole = ParoleState {
9096 original_contributors: [ip1, ip2].into_iter().collect(),
9097 parole_peer: Some(parole_ip),
9098 };
9099
9100 for ip in &parole.original_contributors {
9102 mgr.record_strike(*ip);
9103 }
9104
9105 assert_eq!(*mgr.strikes_map().get(&ip1).unwrap(), 1);
9106 assert_eq!(*mgr.strikes_map().get(&ip2).unwrap(), 1);
9107 assert!(!mgr.strikes_map().contains_key(&parole_ip));
9109
9110 for ip in &parole.original_contributors {
9112 mgr.record_strike(*ip);
9113 }
9114 assert!(mgr.is_banned(&ip1));
9115 assert!(mgr.is_banned(&ip2));
9116 }
9117
9118 #[test]
9119 fn parole_failure_strikes_parole_peer() {
9120 use crate::ban::{BanConfig, BanManager, ParoleState};
9121 use std::net::IpAddr;
9122
9123 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9124 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
9125
9126 let mut mgr = BanManager::new(BanConfig {
9127 max_failures: 2,
9128 use_parole: true,
9129 });
9130
9131 let parole = ParoleState {
9132 original_contributors: [ip1].into_iter().collect(),
9133 parole_peer: Some(parole_ip),
9134 };
9135
9136 if let Some(pp) = parole.parole_peer {
9138 mgr.record_strike(pp);
9139 }
9140
9141 assert_eq!(*mgr.strikes_map().get(&parole_ip).unwrap(), 1);
9142 assert!(!mgr.strikes_map().contains_key(&ip1));
9143 }
9144
9145 #[tokio::test]
9146 async fn banned_peer_rejected_on_connect() {
9147 let data = vec![0xAB; 32768];
9148 let meta = make_test_torrent(&data, 16384);
9149 let storage = make_storage(&data, 16384);
9150 let config = test_config();
9151 let ban_mgr = test_ban_manager();
9152
9153 let banned_ip: std::net::IpAddr = "192.168.1.100".parse().unwrap();
9155 ban_mgr.write().ban(banned_ip);
9156
9157 let (atx, amask) = test_alert_channel();
9158 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9159 let handle = TorrentHandle::from_torrent(
9160 meta,
9161 irontide_core::TorrentVersion::V1Only,
9162 None,
9163 dh,
9164 dm,
9165 config,
9166 test_dht_rx(),
9167 test_dht_rx(),
9168 None,
9169 None,
9170 crate::slot_tuner::SlotTuner::disabled(4),
9171 atx,
9172 amask,
9173 None,
9174 None,
9175 Arc::clone(&ban_mgr),
9176 test_ip_filter(),
9177 Arc::new(Vec::new()),
9178 None,
9179 None,
9180 Arc::new(crate::transport::NetworkFactory::tokio()),
9181 None, Arc::new(crate::stats::SessionCounters::new()),
9183 )
9184 .await
9185 .unwrap();
9186
9187 handle
9189 .add_peers(
9190 vec![
9191 SocketAddr::new(banned_ip, 6881),
9192 "10.0.0.1:6881".parse().unwrap(),
9193 ],
9194 PeerSource::Tracker,
9195 )
9196 .await
9197 .unwrap();
9198
9199 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9200 let stats = handle.stats().await.unwrap();
9201 assert!(
9204 stats.peers_available + stats.peers_connected <= 1,
9205 "banned peer should not be added: available={}, connected={}",
9206 stats.peers_available,
9207 stats.peers_connected
9208 );
9209
9210 handle.shutdown().await.unwrap();
9211 }
9212
9213 #[test]
9214 fn banned_peer_filtered_from_available() {
9215 use crate::ban::{BanConfig, BanManager};
9216 use std::net::IpAddr;
9217
9218 let banned_ip: IpAddr = "192.168.1.200".parse().unwrap();
9219 let ok_ip: IpAddr = "10.0.0.1".parse().unwrap();
9220
9221 let mgr = BanManager::new(BanConfig::default());
9222 assert!(!mgr.is_banned(&banned_ip));
9224 assert!(!mgr.is_banned(&ok_ip));
9225
9226 let mut mgr = BanManager::new(BanConfig::default());
9227 mgr.ban(banned_ip);
9228
9229 assert!(mgr.is_banned(&banned_ip));
9231 assert!(!mgr.is_banned(&ok_ip));
9232 }
9233
9234 #[test]
9237 fn hashing_threads_config_default() {
9238 let s = irontide_settings::Settings::default();
9239 let expected = {
9240 let cores = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
9241 (cores / 4).clamp(2, 8)
9242 };
9243 assert_eq!(s.hashing_threads, expected);
9244 let tc = TorrentConfig::default();
9245 assert_eq!(tc.hashing_threads, expected);
9246 }
9247
9248 #[tokio::test]
9249 async fn checking_state_and_progress_alerts() {
9250 use crate::alert::AlertKind;
9251
9252 let data = vec![0xEEu8; 65536]; let meta = make_test_torrent(&data, 16384);
9254 let storage = make_seeded_storage(&data, 16384);
9255 let config = test_config();
9256
9257 let (atx, amask) = test_alert_channel();
9258 let mut rx = atx.subscribe();
9259 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9260 let handle = TorrentHandle::from_torrent(
9261 meta,
9262 irontide_core::TorrentVersion::V1Only,
9263 None,
9264 dh,
9265 dm,
9266 config,
9267 test_dht_rx(),
9268 test_dht_rx(),
9269 None,
9270 None,
9271 crate::slot_tuner::SlotTuner::disabled(4),
9272 atx,
9273 amask,
9274 None,
9275 None,
9276 test_ban_manager(),
9277 test_ip_filter(),
9278 Arc::new(Vec::new()),
9279 None,
9280 None,
9281 Arc::new(crate::transport::NetworkFactory::tokio()),
9282 None, Arc::new(crate::stats::SessionCounters::new()),
9284 )
9285 .await
9286 .unwrap();
9287
9288 let mut saw_checking = false;
9290 let mut progress_values: Vec<f32> = Vec::new();
9291 let mut saw_checked = false;
9292 let mut checked_have = 0u32;
9293 let mut checked_total = 0u32;
9294
9295 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
9296 while tokio::time::Instant::now() < deadline {
9297 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
9298 Ok(Ok(alert)) => match alert.kind {
9299 AlertKind::StateChanged {
9300 new_state: TorrentState::Checking,
9301 ..
9302 } => {
9303 saw_checking = true;
9304 }
9305 AlertKind::CheckingProgress { progress, .. } => {
9306 progress_values.push(progress);
9307 }
9308 AlertKind::TorrentChecked {
9309 pieces_have,
9310 pieces_total,
9311 ..
9312 } => {
9313 saw_checked = true;
9314 checked_have = pieces_have;
9315 checked_total = pieces_total;
9316 break;
9317 }
9318 _ => {}
9319 },
9320 _ => break,
9321 }
9322 }
9323
9324 assert!(saw_checking, "should have seen StateChanged → Checking");
9325 assert!(
9326 !progress_values.is_empty(),
9327 "should have seen CheckingProgress alerts"
9328 );
9329 for w in progress_values.windows(2) {
9331 assert!(
9332 w[1] >= w[0],
9333 "progress should be monotonically increasing: {} < {}",
9334 w[0],
9335 w[1]
9336 );
9337 }
9338 assert!(saw_checked, "should have seen TorrentChecked");
9339 assert_eq!(checked_have, 4);
9340 assert_eq!(checked_total, 4);
9341
9342 tokio::time::sleep(Duration::from_millis(50)).await;
9344 let stats = handle.stats().await.unwrap();
9345 assert_eq!(stats.state, TorrentState::Seeding);
9346
9347 handle.shutdown().await.unwrap();
9348 }
9349
9350 #[tokio::test]
9351 #[allow(clippy::float_cmp, reason = "exact sentinel value comparison (0.0)")]
9352 async fn checking_progress_in_stats() {
9353 let data = vec![0xAB; 32768];
9355 let meta = make_test_torrent(&data, 16384);
9356 let storage = make_storage(&data, 16384);
9357 let config = test_config();
9358
9359 let (atx, amask) = test_alert_channel();
9360 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9361 let handle = TorrentHandle::from_torrent(
9362 meta,
9363 irontide_core::TorrentVersion::V1Only,
9364 None,
9365 dh,
9366 dm,
9367 config,
9368 test_dht_rx(),
9369 test_dht_rx(),
9370 None,
9371 None,
9372 crate::slot_tuner::SlotTuner::disabled(4),
9373 atx,
9374 amask,
9375 None,
9376 None,
9377 test_ban_manager(),
9378 test_ip_filter(),
9379 Arc::new(Vec::new()),
9380 None,
9381 None,
9382 Arc::new(crate::transport::NetworkFactory::tokio()),
9383 None, Arc::new(crate::stats::SessionCounters::new()),
9385 )
9386 .await
9387 .unwrap();
9388
9389 tokio::time::sleep(Duration::from_millis(100)).await;
9391
9392 let stats = handle.stats().await.unwrap();
9393 assert_eq!(stats.state, TorrentState::Downloading);
9394 assert_eq!(
9395 stats.checking_progress, 0.0,
9396 "checking_progress should be 0.0 when not checking"
9397 );
9398
9399 handle.shutdown().await.unwrap();
9400 }
9401
9402 #[tokio::test]
9403 async fn verify_pieces_partial_data() {
9404 use crate::alert::AlertKind;
9405
9406 let data = vec![0xCCu8; 65536]; let meta = make_test_torrent(&data, 16384);
9409
9410 let lengths = Lengths::new(data.len() as u64, 16384, DEFAULT_CHUNK_SIZE);
9412 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
9413 for p in 0..2u32 {
9414 let offset = lengths.piece_offset(p) as usize;
9415 let size = lengths.piece_size(p) as usize;
9416 storage
9417 .write_chunk(p, 0, &data[offset..offset + size])
9418 .unwrap();
9419 }
9420 let config = test_config();
9423 let (atx, amask) = test_alert_channel();
9424 let mut rx = atx.subscribe();
9425 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9426 let handle = TorrentHandle::from_torrent(
9427 meta,
9428 irontide_core::TorrentVersion::V1Only,
9429 None,
9430 dh,
9431 dm,
9432 config,
9433 test_dht_rx(),
9434 test_dht_rx(),
9435 None,
9436 None,
9437 crate::slot_tuner::SlotTuner::disabled(4),
9438 atx,
9439 amask,
9440 None,
9441 None,
9442 test_ban_manager(),
9443 test_ip_filter(),
9444 Arc::new(Vec::new()),
9445 None,
9446 None,
9447 Arc::new(crate::transport::NetworkFactory::tokio()),
9448 None, Arc::new(crate::stats::SessionCounters::new()),
9450 )
9451 .await
9452 .unwrap();
9453
9454 let mut checked_have = 0u32;
9456 let mut checked_total = 0u32;
9457 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
9458 while tokio::time::Instant::now() < deadline {
9459 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
9460 Ok(Ok(alert)) => {
9461 if let AlertKind::TorrentChecked {
9462 pieces_have,
9463 pieces_total,
9464 ..
9465 } = alert.kind
9466 {
9467 checked_have = pieces_have;
9468 checked_total = pieces_total;
9469 break;
9470 }
9471 }
9472 _ => break,
9473 }
9474 }
9475
9476 assert_eq!(checked_have, 2, "only 2 pieces should be valid");
9477 assert_eq!(checked_total, 4);
9478
9479 tokio::time::sleep(Duration::from_millis(50)).await;
9481 let stats = handle.stats().await.unwrap();
9482 assert_eq!(stats.state, TorrentState::Downloading);
9483 assert_eq!(stats.pieces_have, 2);
9484 assert_eq!(stats.pieces_total, 4);
9485
9486 handle.shutdown().await.unwrap();
9487 }
9488
9489 #[tokio::test]
9492 async fn ip_filter_blocks_peers_in_handle_add_peers() {
9493 let data = vec![0xCD; 32768];
9494 let meta = make_test_torrent(&data, 16384);
9495 let storage = make_storage(&data, 16384);
9496 let config = test_config();
9497
9498 let ip_filter = {
9500 let mut f = crate::ip_filter::IpFilter::new();
9501 f.add_rule(
9502 "203.0.113.0".parse().unwrap(),
9503 "203.0.113.255".parse().unwrap(),
9504 1,
9505 );
9506 Arc::new(parking_lot::RwLock::new(f))
9507 };
9508
9509 let (atx, amask) = test_alert_channel();
9510 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9511 let handle = TorrentHandle::from_torrent(
9512 meta,
9513 irontide_core::TorrentVersion::V1Only,
9514 None,
9515 dh,
9516 dm,
9517 config,
9518 test_dht_rx(),
9519 test_dht_rx(),
9520 None,
9521 None,
9522 crate::slot_tuner::SlotTuner::disabled(4),
9523 atx,
9524 amask,
9525 None,
9526 None,
9527 test_ban_manager(),
9528 Arc::clone(&ip_filter),
9529 Arc::new(Vec::new()),
9530 None,
9531 None,
9532 Arc::new(crate::transport::NetworkFactory::tokio()),
9533 None, Arc::new(crate::stats::SessionCounters::new()),
9535 )
9536 .await
9537 .unwrap();
9538
9539 let blocked_addr: SocketAddr = "203.0.113.42:6881".parse().unwrap();
9541 let allowed_addr: SocketAddr = "198.51.100.1:6881".parse().unwrap();
9542 handle
9543 .add_peers(vec![blocked_addr, allowed_addr], PeerSource::Tracker)
9544 .await
9545 .unwrap();
9546
9547 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9548 let stats = handle.stats().await.unwrap();
9549 assert!(
9551 stats.peers_available + stats.peers_connected <= 1,
9552 "blocked peer should not be added: available={}, connected={}",
9553 stats.peers_available,
9554 stats.peers_connected
9555 );
9556
9557 handle.shutdown().await.unwrap();
9558 }
9559
9560 #[tokio::test]
9561 async fn set_ip_filter_replaces_filter_and_blocks_new_ip() {
9562 let data = vec![0xCD; 32768];
9565 let meta = make_test_torrent(&data, 16384);
9566 let storage = make_storage(&data, 16384);
9567 let config = test_config();
9568
9569 let ip_filter: irontide_session_types::SharedIpFilter =
9571 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
9572
9573 let (atx, amask) = test_alert_channel();
9574 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9575 let handle = TorrentHandle::from_torrent(
9576 meta,
9577 irontide_core::TorrentVersion::V1Only,
9578 None,
9579 dh,
9580 dm,
9581 config,
9582 test_dht_rx(),
9583 test_dht_rx(),
9584 None,
9585 None,
9586 crate::slot_tuner::SlotTuner::disabled(4),
9587 atx,
9588 amask,
9589 None,
9590 None,
9591 test_ban_manager(),
9592 Arc::clone(&ip_filter),
9593 Arc::new(Vec::new()),
9594 None,
9595 None,
9596 Arc::new(crate::transport::NetworkFactory::tokio()),
9597 None, Arc::new(crate::stats::SessionCounters::new()),
9599 )
9600 .await
9601 .unwrap();
9602
9603 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9606 let local_addr = listener.local_addr().unwrap();
9607 handle
9608 .add_peers(vec![local_addr], PeerSource::Tracker)
9609 .await
9610 .unwrap();
9611 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9612 let stats = handle.stats().await.unwrap();
9613 assert!(
9614 stats.peers_available + stats.peers_connected >= 1,
9615 "peer should be allowed initially"
9616 );
9617 handle.shutdown().await.unwrap();
9618
9619 {
9621 let mut f = ip_filter.write();
9622 f.add_rule(
9623 "198.51.100.0".parse().unwrap(),
9624 "198.51.100.255".parse().unwrap(),
9625 1,
9626 );
9627 }
9628
9629 assert!(ip_filter.read().is_blocked("198.51.100.1".parse().unwrap()));
9631 assert!(!ip_filter.read().is_blocked("203.0.113.1".parse().unwrap()));
9633 }
9634
9635 #[test]
9636 fn relocate_files_moves_and_cleans_up() {
9637 let tmp = std::env::temp_dir().join(format!("torrent_relocate_{}", std::process::id()));
9638 let src = tmp.join("src");
9639 let dst = tmp.join("dst");
9640
9641 let subdir = src.join("TorrentName").join("subdir");
9645 std::fs::create_dir_all(&subdir).unwrap();
9646 std::fs::write(subdir.join("file1.txt"), b"hello").unwrap();
9647 std::fs::write(src.join("TorrentName").join("file2.txt"), b"world").unwrap();
9648
9649 let file_paths = vec![
9650 std::path::PathBuf::from("TorrentName/subdir/file1.txt"),
9651 std::path::PathBuf::from("TorrentName/file2.txt"),
9652 ];
9653
9654 relocate_files(&src, &dst, &file_paths).unwrap();
9655
9656 assert_eq!(
9658 std::fs::read_to_string(dst.join("TorrentName/subdir/file1.txt")).unwrap(),
9659 "hello"
9660 );
9661 assert_eq!(
9662 std::fs::read_to_string(dst.join("TorrentName/file2.txt")).unwrap(),
9663 "world"
9664 );
9665
9666 assert!(!src.join("TorrentName").join("subdir").exists());
9668 assert!(!src.join("TorrentName").exists());
9669
9670 let _ = std::fs::remove_dir_all(&tmp);
9672 }
9673
9674 #[test]
9675 fn relocate_files_skips_missing() {
9676 let tmp =
9677 std::env::temp_dir().join(format!("torrent_relocate_skip_{}", std::process::id()));
9678 let src = tmp.join("src");
9679 let dst = tmp.join("dst");
9680 std::fs::create_dir_all(&src).unwrap();
9681
9682 let file_paths = vec![std::path::PathBuf::from("nonexistent.txt")];
9684 relocate_files(&src, &dst, &file_paths).unwrap();
9685
9686 assert!(!dst.join("nonexistent.txt").exists());
9687
9688 let _ = std::fs::remove_dir_all(&tmp);
9689 }
9690
9691 #[tokio::test]
9694 async fn force_recheck_transitions_to_checking() {
9695 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
9697 let storage = make_seeded_storage(&data, 16384);
9698 let config = test_config();
9699
9700 let (atx, amask) = test_alert_channel();
9701 let mut arx = atx.subscribe();
9702 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9703 let handle = TorrentHandle::from_torrent(
9704 meta,
9705 irontide_core::TorrentVersion::V1Only,
9706 None,
9707 dh,
9708 dm,
9709 config,
9710 test_dht_rx(),
9711 test_dht_rx(),
9712 None,
9713 None,
9714 crate::slot_tuner::SlotTuner::disabled(4),
9715 atx,
9716 amask,
9717 None,
9718 None,
9719 test_ban_manager(),
9720 test_ip_filter(),
9721 Arc::new(Vec::new()),
9722 None,
9723 None,
9724 Arc::new(crate::transport::NetworkFactory::tokio()),
9725 None, Arc::new(crate::stats::SessionCounters::new()),
9727 )
9728 .await
9729 .unwrap();
9730
9731 tokio::time::sleep(Duration::from_millis(100)).await;
9733 let stats = handle.stats().await.unwrap();
9734 assert_eq!(stats.state, TorrentState::Seeding, "should start as seeder");
9735
9736 while arx.try_recv().is_ok() {}
9738
9739 handle.force_recheck().await.unwrap();
9741
9742 let mut saw_checking = false;
9745 while let Ok(alert) = arx.try_recv() {
9746 if let crate::alert::AlertKind::StateChanged { new_state, .. } = alert.kind
9747 && new_state == TorrentState::Checking
9748 {
9749 saw_checking = true;
9750 }
9751 }
9752 assert!(
9753 saw_checking,
9754 "should have transitioned through Checking state"
9755 );
9756
9757 handle.shutdown().await.unwrap();
9758 }
9759
9760 #[tokio::test]
9763 async fn force_recheck_completes() {
9764 let data = vec![0xEEu8; 32768]; let meta = make_test_torrent(&data, 16384);
9766 let storage = make_seeded_storage(&data, 16384);
9767 let config = test_config();
9768
9769 let (atx, amask) = test_alert_channel();
9770 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9771 let handle = TorrentHandle::from_torrent(
9772 meta,
9773 irontide_core::TorrentVersion::V1Only,
9774 None,
9775 dh,
9776 dm,
9777 config,
9778 test_dht_rx(),
9779 test_dht_rx(),
9780 None,
9781 None,
9782 crate::slot_tuner::SlotTuner::disabled(4),
9783 atx,
9784 amask,
9785 None,
9786 None,
9787 test_ban_manager(),
9788 test_ip_filter(),
9789 Arc::new(Vec::new()),
9790 None,
9791 None,
9792 Arc::new(crate::transport::NetworkFactory::tokio()),
9793 None, Arc::new(crate::stats::SessionCounters::new()),
9795 )
9796 .await
9797 .unwrap();
9798
9799 tokio::time::sleep(Duration::from_millis(100)).await;
9801 let stats = handle.stats().await.unwrap();
9802 assert_eq!(stats.state, TorrentState::Seeding);
9803 assert_eq!(stats.pieces_have, 2);
9804
9805 handle.force_recheck().await.unwrap();
9807
9808 let stats = handle.stats().await.unwrap();
9809 assert_eq!(
9810 stats.state,
9811 TorrentState::Seeding,
9812 "should return to Seeding after recheck"
9813 );
9814 assert_eq!(stats.pieces_have, 2, "all pieces should still be verified");
9815
9816 handle.shutdown().await.unwrap();
9817 }
9818
9819 #[tokio::test]
9822 async fn rename_file_succeeds() {
9823 let tmp = std::env::temp_dir().join(format!("torrent_rename_{}", std::process::id()));
9825 std::fs::create_dir_all(&tmp).unwrap();
9826
9827 let data = vec![0xFFu8; 16384]; let meta = make_test_torrent(&data, 16384);
9829 let storage = make_seeded_storage(&data, 16384);
9830
9831 std::fs::write(tmp.join("test"), &data).unwrap();
9834
9835 let mut config = test_config();
9836 config.download_dir = tmp.clone();
9837
9838 let (atx, amask) = test_alert_channel();
9839 let mut arx = atx.subscribe();
9840 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9841 let handle = TorrentHandle::from_torrent(
9842 meta,
9843 irontide_core::TorrentVersion::V1Only,
9844 None,
9845 dh,
9846 dm,
9847 config,
9848 test_dht_rx(),
9849 test_dht_rx(),
9850 None,
9851 None,
9852 crate::slot_tuner::SlotTuner::disabled(4),
9853 atx,
9854 amask,
9855 None,
9856 None,
9857 test_ban_manager(),
9858 test_ip_filter(),
9859 Arc::new(Vec::new()),
9860 None,
9861 None,
9862 Arc::new(crate::transport::NetworkFactory::tokio()),
9863 None, Arc::new(crate::stats::SessionCounters::new()),
9865 )
9866 .await
9867 .unwrap();
9868
9869 tokio::time::sleep(Duration::from_millis(100)).await;
9871
9872 while arx.try_recv().is_ok() {}
9874
9875 handle.rename_file(0, "test_renamed".into()).await.unwrap();
9877
9878 assert!(!tmp.join("test").exists(), "old file should be removed");
9880 assert!(tmp.join("test_renamed").exists(), "new file should exist");
9881
9882 let mut saw_renamed = false;
9884 while let Ok(alert) = arx.try_recv() {
9885 if let AlertKind::FileRenamed { index, .. } = alert.kind {
9886 assert_eq!(index, 0);
9887 saw_renamed = true;
9888 }
9889 }
9890 assert!(saw_renamed, "should have received FileRenamed alert");
9891
9892 handle.shutdown().await.unwrap();
9893 let _ = std::fs::remove_dir_all(&tmp);
9894 }
9895
9896 #[tokio::test]
9899 async fn rename_file_invalid_index_errors() {
9900 let data = vec![0xCCu8; 16384]; let meta = make_test_torrent(&data, 16384);
9902 let storage = make_seeded_storage(&data, 16384);
9903 let config = test_config();
9904
9905 let (atx, amask) = test_alert_channel();
9906 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9907 let handle = TorrentHandle::from_torrent(
9908 meta,
9909 irontide_core::TorrentVersion::V1Only,
9910 None,
9911 dh,
9912 dm,
9913 config,
9914 test_dht_rx(),
9915 test_dht_rx(),
9916 None,
9917 None,
9918 crate::slot_tuner::SlotTuner::disabled(4),
9919 atx,
9920 amask,
9921 None,
9922 None,
9923 test_ban_manager(),
9924 test_ip_filter(),
9925 Arc::new(Vec::new()),
9926 None,
9927 None,
9928 Arc::new(crate::transport::NetworkFactory::tokio()),
9929 None, Arc::new(crate::stats::SessionCounters::new()),
9931 )
9932 .await
9933 .unwrap();
9934
9935 tokio::time::sleep(Duration::from_millis(100)).await;
9937
9938 let result = handle.rename_file(99, "bad".into()).await;
9940 assert!(result.is_err(), "should fail for out-of-range file index");
9941
9942 handle.shutdown().await.unwrap();
9943 }
9944
9945 #[tokio::test]
9948 async fn file_completed_alert_fires() {
9949 let data = vec![0xBBu8; 32768]; let meta = make_test_torrent(&data, 16384);
9951 let storage = make_seeded_storage(&data, 16384);
9952 let config = test_config();
9953
9954 let (atx, amask) = test_alert_channel();
9955 let mut arx = atx.subscribe();
9956 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9957 let handle = TorrentHandle::from_torrent(
9958 meta,
9959 irontide_core::TorrentVersion::V1Only,
9960 None,
9961 dh,
9962 dm,
9963 config,
9964 test_dht_rx(),
9965 test_dht_rx(),
9966 None,
9967 None,
9968 crate::slot_tuner::SlotTuner::disabled(4),
9969 atx,
9970 amask,
9971 None,
9972 None,
9973 test_ban_manager(),
9974 test_ip_filter(),
9975 Arc::new(Vec::new()),
9976 None,
9977 None,
9978 Arc::new(crate::transport::NetworkFactory::tokio()),
9979 None, Arc::new(crate::stats::SessionCounters::new()),
9981 )
9982 .await
9983 .unwrap();
9984
9985 tokio::time::sleep(Duration::from_millis(200)).await;
9987
9988 let mut saw_file_completed = false;
9990 while let Ok(alert) = arx.try_recv() {
9991 if let AlertKind::FileCompleted { file_index, .. } = alert.kind {
9992 assert_eq!(file_index, 0, "should be file index 0");
9993 saw_file_completed = true;
9994 }
9995 }
9996 assert!(
9997 saw_file_completed,
9998 "should have received FileCompleted alert"
9999 );
10000
10001 handle.shutdown().await.unwrap();
10002 }
10003
10004 #[test]
10007 fn metadata_failed_alert_fires() {
10008 let info_hash = Id20::from([0u8; 20]);
10010 let alert = crate::alert::Alert::new(AlertKind::MetadataFailed { info_hash });
10011 assert!(
10012 alert
10013 .category()
10014 .contains(crate::alert::AlertCategory::STATUS),
10015 "MetadataFailed should have STATUS category"
10016 );
10017 assert!(
10018 alert
10019 .category()
10020 .contains(crate::alert::AlertCategory::ERROR),
10021 "MetadataFailed should have ERROR category"
10022 );
10023
10024 let (tx, mut rx) = broadcast::channel(16);
10026 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
10027 post_alert(&tx, &mask, AlertKind::MetadataFailed { info_hash });
10028 let received = rx.try_recv().expect("should receive MetadataFailed alert");
10029 assert!(matches!(received.kind, AlertKind::MetadataFailed { .. }));
10030 }
10031
10032 #[tokio::test]
10035 async fn set_max_connections_persists() {
10036 let data = vec![0xAB; 32768];
10037 let meta = make_test_torrent(&data, 16384);
10038 let storage = make_storage(&data, 16384);
10039 let config = test_config();
10040
10041 let (atx, amask) = test_alert_channel();
10042 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10043 let handle = TorrentHandle::from_torrent(
10044 meta,
10045 irontide_core::TorrentVersion::V1Only,
10046 None,
10047 dh,
10048 dm,
10049 config,
10050 test_dht_rx(),
10051 test_dht_rx(),
10052 None,
10053 None,
10054 crate::slot_tuner::SlotTuner::disabled(4),
10055 atx,
10056 amask,
10057 None,
10058 None,
10059 test_ban_manager(),
10060 test_ip_filter(),
10061 Arc::new(Vec::new()),
10062 None,
10063 None,
10064 Arc::new(crate::transport::NetworkFactory::tokio()),
10065 None, Arc::new(crate::stats::SessionCounters::new()),
10067 )
10068 .await
10069 .unwrap();
10070
10071 handle.set_max_connections(10).await.unwrap();
10073 let val = handle.max_connections().await.unwrap();
10074 assert_eq!(val, 10);
10075
10076 handle.set_max_connections(25).await.unwrap();
10078 let val = handle.max_connections().await.unwrap();
10079 assert_eq!(val, 25);
10080
10081 let stats = handle.stats().await.unwrap();
10083 assert_eq!(stats.connections_limit, 25);
10084
10085 handle.shutdown().await.unwrap();
10086 }
10087
10088 #[tokio::test]
10091 async fn max_connections_default() {
10092 let data = vec![0xAB; 32768];
10093 let meta = make_test_torrent(&data, 16384);
10094 let storage = make_storage(&data, 16384);
10095 let config = test_config();
10096 let expected_default = config.max_peers;
10097
10098 let (atx, amask) = test_alert_channel();
10099 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10100 let handle = TorrentHandle::from_torrent(
10101 meta,
10102 irontide_core::TorrentVersion::V1Only,
10103 None,
10104 dh,
10105 dm,
10106 config,
10107 test_dht_rx(),
10108 test_dht_rx(),
10109 None,
10110 None,
10111 crate::slot_tuner::SlotTuner::disabled(4),
10112 atx,
10113 amask,
10114 None,
10115 None,
10116 test_ban_manager(),
10117 test_ip_filter(),
10118 Arc::new(Vec::new()),
10119 None,
10120 None,
10121 Arc::new(crate::transport::NetworkFactory::tokio()),
10122 None, Arc::new(crate::stats::SessionCounters::new()),
10124 )
10125 .await
10126 .unwrap();
10127
10128 let val = handle.max_connections().await.unwrap();
10130 assert_eq!(val, 0);
10131
10132 let stats = handle.stats().await.unwrap();
10134 assert_eq!(stats.connections_limit, expected_default);
10135
10136 handle.shutdown().await.unwrap();
10137 }
10138
10139 #[tokio::test]
10142 async fn set_max_uploads_round_trip() {
10143 let data = vec![0xAB; 32768];
10144 let meta = make_test_torrent(&data, 16384);
10145 let storage = make_storage(&data, 16384);
10146 let config = test_config();
10147
10148 let (atx, amask) = test_alert_channel();
10149 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10150 let handle = TorrentHandle::from_torrent(
10151 meta,
10152 irontide_core::TorrentVersion::V1Only,
10153 None,
10154 dh,
10155 dm,
10156 config,
10157 test_dht_rx(),
10158 test_dht_rx(),
10159 None,
10160 None,
10161 crate::slot_tuner::SlotTuner::disabled(4),
10162 atx,
10163 amask,
10164 None,
10165 None,
10166 test_ban_manager(),
10167 test_ip_filter(),
10168 Arc::new(Vec::new()),
10169 None,
10170 None,
10171 Arc::new(crate::transport::NetworkFactory::tokio()),
10172 None, Arc::new(crate::stats::SessionCounters::new()),
10174 )
10175 .await
10176 .unwrap();
10177
10178 handle.set_max_uploads(8).await.unwrap();
10180 let val = handle.max_uploads().await.unwrap();
10181 assert_eq!(val, 8);
10182
10183 let stats = handle.stats().await.unwrap();
10185 assert_eq!(stats.uploads_limit, 8);
10186
10187 handle.shutdown().await.unwrap();
10188 }
10189
10190 #[tokio::test]
10193 async fn external_ip_detected_alert() {
10194 let data = vec![0xAB; 32768];
10195 let meta = make_test_torrent(&data, 16384);
10196 let storage = make_storage(&data, 16384);
10197 let config = test_config();
10198
10199 let (atx, amask) = test_alert_channel();
10200 let mut arx = atx.subscribe();
10201 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10202 let handle = TorrentHandle::from_torrent(
10203 meta,
10204 irontide_core::TorrentVersion::V1Only,
10205 None,
10206 dh,
10207 dm,
10208 config,
10209 test_dht_rx(),
10210 test_dht_rx(),
10211 None,
10212 None,
10213 crate::slot_tuner::SlotTuner::disabled(4),
10214 atx,
10215 amask,
10216 None,
10217 None,
10218 test_ban_manager(),
10219 test_ip_filter(),
10220 Arc::new(Vec::new()),
10221 None,
10222 None,
10223 Arc::new(crate::transport::NetworkFactory::tokio()),
10224 None, Arc::new(crate::stats::SessionCounters::new()),
10226 )
10227 .await
10228 .unwrap();
10229
10230 while arx.try_recv().is_ok() {}
10232
10233 let test_ip: std::net::IpAddr = "203.0.113.42".parse().unwrap();
10235 handle
10236 .cmd_tx
10237 .send(TorrentCommand::UpdateExternalIp { ip: test_ip })
10238 .await
10239 .unwrap();
10240
10241 tokio::time::sleep(Duration::from_millis(50)).await;
10243
10244 let mut saw_alert = false;
10246 while let Ok(alert) = arx.try_recv() {
10247 if let AlertKind::ExternalIpDetected { ip } = alert.kind {
10248 assert_eq!(ip, test_ip);
10249 saw_alert = true;
10250 }
10251 }
10252 assert!(saw_alert, "should have received ExternalIpDetected alert");
10253
10254 handle.shutdown().await.unwrap();
10255 }
10256
10257 #[tokio::test]
10260 async fn get_peer_info_returns_connected_peers() {
10261 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
10264 let config = test_config();
10265
10266 let (atx, amask) = test_alert_channel();
10267 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10268 let handle = TorrentHandle::from_torrent(
10269 meta.clone(),
10270 irontide_core::TorrentVersion::V1Only,
10271 None,
10272 dh,
10273 dm,
10274 config,
10275 test_dht_rx(),
10276 test_dht_rx(),
10277 None,
10278 None,
10279 crate::slot_tuner::SlotTuner::disabled(4),
10280 atx,
10281 amask,
10282 None,
10283 None,
10284 test_ban_manager(),
10285 test_ip_filter(),
10286 Arc::new(Vec::new()),
10287 None,
10288 None,
10289 Arc::new(crate::transport::NetworkFactory::tokio()),
10290 None, Arc::new(crate::stats::SessionCounters::new()),
10292 )
10293 .await
10294 .unwrap();
10295
10296 let stats = handle.stats().await.unwrap();
10298 let listen_port = stats.peers_connected; let peer_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
10302 let peer_addr = peer_listener.local_addr().unwrap();
10303
10304 handle
10305 .add_peers(vec![peer_addr], PeerSource::Tracker)
10306 .await
10307 .unwrap();
10308
10309 let accept_timeout =
10311 tokio::time::timeout(Duration::from_secs(2), peer_listener.accept()).await;
10312 if let Ok(Ok((mut stream, _))) = accept_timeout {
10313 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
10315 if tokio::time::timeout(Duration::from_millis(500), stream.read_exact(&mut hs_buf))
10316 .await
10317 .is_ok()
10318 {
10319 let hs = Handshake::new(meta.info_hash, Id20::from([0xBB; 20]));
10321 let hs_bytes = hs.to_bytes();
10322 let _ = stream.write_all(&hs_bytes).await;
10323
10324 tokio::time::sleep(Duration::from_millis(200)).await;
10326
10327 let peer_info = handle.get_peer_info().await.unwrap();
10329 if !peer_info.is_empty() {
10331 let p = &peer_info[0];
10332 assert!(p.peer_choking, "peer should be choking us initially");
10334 assert!(
10336 !p.am_choking,
10337 "we should not be choking peer after connect (M107 unconditional unchoke)"
10338 );
10339 assert!(
10340 !p.peer_interested,
10341 "peer should not be interested initially"
10342 );
10343 assert_eq!(p.num_pieces, 0);
10344 assert_eq!(p.source, PeerSource::Tracker);
10345 }
10346 }
10347 }
10348 let _ = handle.get_peer_info().await.unwrap();
10350 assert_eq!(listen_port, 0); handle.shutdown().await.unwrap();
10353 }
10354
10355 #[tokio::test]
10358 async fn get_peer_info_empty_when_no_peers() {
10359 let data = vec![0xAB; 32768];
10360 let meta = make_test_torrent(&data, 16384);
10361 let storage = make_storage(&data, 16384);
10362 let config = test_config();
10363
10364 let (atx, amask) = test_alert_channel();
10365 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10366 let handle = TorrentHandle::from_torrent(
10367 meta,
10368 irontide_core::TorrentVersion::V1Only,
10369 None,
10370 dh,
10371 dm,
10372 config,
10373 test_dht_rx(),
10374 test_dht_rx(),
10375 None,
10376 None,
10377 crate::slot_tuner::SlotTuner::disabled(4),
10378 atx,
10379 amask,
10380 None,
10381 None,
10382 test_ban_manager(),
10383 test_ip_filter(),
10384 Arc::new(Vec::new()),
10385 None,
10386 None,
10387 Arc::new(crate::transport::NetworkFactory::tokio()),
10388 None, Arc::new(crate::stats::SessionCounters::new()),
10390 )
10391 .await
10392 .unwrap();
10393
10394 let peer_info = handle.get_peer_info().await.unwrap();
10395 assert!(peer_info.is_empty(), "should have no peers initially");
10396
10397 handle.shutdown().await.unwrap();
10398 }
10399
10400 #[tokio::test]
10403 async fn get_download_queue_empty_initially() {
10404 let data = vec![0xAB; 32768];
10405 let meta = make_test_torrent(&data, 16384);
10406 let storage = make_storage(&data, 16384);
10407 let config = test_config();
10408
10409 let (atx, amask) = test_alert_channel();
10410 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10411 let handle = TorrentHandle::from_torrent(
10412 meta,
10413 irontide_core::TorrentVersion::V1Only,
10414 None,
10415 dh,
10416 dm,
10417 config,
10418 test_dht_rx(),
10419 test_dht_rx(),
10420 None,
10421 None,
10422 crate::slot_tuner::SlotTuner::disabled(4),
10423 atx,
10424 amask,
10425 None,
10426 None,
10427 test_ban_manager(),
10428 test_ip_filter(),
10429 Arc::new(Vec::new()),
10430 None,
10431 None,
10432 Arc::new(crate::transport::NetworkFactory::tokio()),
10433 None, Arc::new(crate::stats::SessionCounters::new()),
10435 )
10436 .await
10437 .unwrap();
10438
10439 let queue = handle.get_download_queue().await.unwrap();
10440 assert!(
10441 queue.is_empty(),
10442 "download queue should be empty with no active downloads"
10443 );
10444
10445 handle.shutdown().await.unwrap();
10446 }
10447
10448 #[tokio::test]
10451 async fn have_piece_false_initially() {
10452 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10454 let storage = make_storage(&data, 16384);
10455 let config = test_config();
10456
10457 let (atx, amask) = test_alert_channel();
10458 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10459 let handle = TorrentHandle::from_torrent(
10460 meta,
10461 irontide_core::TorrentVersion::V1Only,
10462 None,
10463 dh,
10464 dm,
10465 config,
10466 test_dht_rx(),
10467 test_dht_rx(),
10468 None,
10469 None,
10470 crate::slot_tuner::SlotTuner::disabled(4),
10471 atx,
10472 amask,
10473 None,
10474 None,
10475 test_ban_manager(),
10476 test_ip_filter(),
10477 Arc::new(Vec::new()),
10478 None,
10479 None,
10480 Arc::new(crate::transport::NetworkFactory::tokio()),
10481 None, Arc::new(crate::stats::SessionCounters::new()),
10483 )
10484 .await
10485 .unwrap();
10486
10487 assert!(
10488 !handle.have_piece(0).await.unwrap(),
10489 "piece 0 should not be downloaded initially"
10490 );
10491 assert!(
10492 !handle.have_piece(1).await.unwrap(),
10493 "piece 1 should not be downloaded initially"
10494 );
10495
10496 handle.shutdown().await.unwrap();
10497 }
10498
10499 #[tokio::test]
10502 async fn piece_availability_empty_no_peers() {
10503 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10505 let storage = make_storage(&data, 16384);
10506 let config = test_config();
10507
10508 let (atx, amask) = test_alert_channel();
10509 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10510 let handle = TorrentHandle::from_torrent(
10511 meta,
10512 irontide_core::TorrentVersion::V1Only,
10513 None,
10514 dh,
10515 dm,
10516 config,
10517 test_dht_rx(),
10518 test_dht_rx(),
10519 None,
10520 None,
10521 crate::slot_tuner::SlotTuner::disabled(4),
10522 atx,
10523 amask,
10524 None,
10525 None,
10526 test_ban_manager(),
10527 test_ip_filter(),
10528 Arc::new(Vec::new()),
10529 None,
10530 None,
10531 Arc::new(crate::transport::NetworkFactory::tokio()),
10532 None, Arc::new(crate::stats::SessionCounters::new()),
10534 )
10535 .await
10536 .unwrap();
10537
10538 let avail = handle.piece_availability().await.unwrap();
10539 assert_eq!(avail.len(), 2, "should have availability for 2 pieces");
10540 assert!(
10541 avail.iter().all(|&c| c == 0),
10542 "all availability counts should be 0 with no peers"
10543 );
10544
10545 handle.shutdown().await.unwrap();
10546 }
10547
10548 #[tokio::test]
10551 async fn file_progress_zeros_initially() {
10552 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10554 let storage = make_storage(&data, 16384);
10555 let config = test_config();
10556
10557 let (atx, amask) = test_alert_channel();
10558 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10559 let handle = TorrentHandle::from_torrent(
10560 meta,
10561 irontide_core::TorrentVersion::V1Only,
10562 None,
10563 dh,
10564 dm,
10565 config,
10566 test_dht_rx(),
10567 test_dht_rx(),
10568 None,
10569 None,
10570 crate::slot_tuner::SlotTuner::disabled(4),
10571 atx,
10572 amask,
10573 None,
10574 None,
10575 test_ban_manager(),
10576 test_ip_filter(),
10577 Arc::new(Vec::new()),
10578 None,
10579 None,
10580 Arc::new(crate::transport::NetworkFactory::tokio()),
10581 None, Arc::new(crate::stats::SessionCounters::new()),
10583 )
10584 .await
10585 .unwrap();
10586
10587 let progress = handle.file_progress().await.unwrap();
10588 assert_eq!(progress.len(), 1, "single-file torrent should have 1 entry");
10589 assert_eq!(progress[0], 0, "no bytes should be downloaded initially");
10590
10591 handle.shutdown().await.unwrap();
10592 }
10593
10594 fn make_test_torrent_multi(
10598 data: &[u8],
10599 piece_length: u64,
10600 file_lengths: &[u64],
10601 ) -> TorrentMetaV1 {
10602 use serde::Serialize;
10603
10604 #[derive(Serialize)]
10605 struct FileE {
10606 length: u64,
10607 path: Vec<String>,
10608 }
10609
10610 #[derive(Serialize)]
10611 struct Info<'a> {
10612 name: &'a str,
10613 #[serde(rename = "piece length")]
10614 piece_length: u64,
10615 #[serde(with = "serde_bytes")]
10616 pieces: &'a [u8],
10617 files: Vec<FileE>,
10618 }
10619
10620 #[derive(Serialize)]
10621 struct Torrent<'a> {
10622 info: Info<'a>,
10623 }
10624
10625 let mut pieces = Vec::new();
10626 let mut offset = 0;
10627 while offset < data.len() {
10628 let end = (offset + piece_length as usize).min(data.len());
10629 let hash = irontide_core::sha1(&data[offset..end]);
10630 pieces.extend_from_slice(hash.as_bytes());
10631 offset = end;
10632 }
10633
10634 let files: Vec<FileE> = file_lengths
10635 .iter()
10636 .enumerate()
10637 .map(|(i, &len)| FileE {
10638 length: len,
10639 path: vec![format!("file{i}.bin")],
10640 })
10641 .collect();
10642
10643 let t = Torrent {
10644 info: Info {
10645 name: "test_multi",
10646 piece_length,
10647 pieces: &pieces,
10648 files,
10649 },
10650 };
10651
10652 let bytes = irontide_bencode::to_bytes(&t).unwrap();
10653 torrent_from_bytes(&bytes).unwrap()
10654 }
10655
10656 #[tokio::test]
10657 async fn file_progress_length_matches_file_count() {
10658 let data = vec![0xCD; 32768];
10660 let file_lengths = [10000u64, 20000, 2768];
10661 let meta = make_test_torrent_multi(&data, 16384, &file_lengths);
10662 let storage = make_storage(&data, 16384);
10663 let config = test_config();
10664
10665 let (atx, amask) = test_alert_channel();
10666 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10667 let handle = TorrentHandle::from_torrent(
10668 meta,
10669 irontide_core::TorrentVersion::V1Only,
10670 None,
10671 dh,
10672 dm,
10673 config,
10674 test_dht_rx(),
10675 test_dht_rx(),
10676 None,
10677 None,
10678 crate::slot_tuner::SlotTuner::disabled(4),
10679 atx,
10680 amask,
10681 None,
10682 None,
10683 test_ban_manager(),
10684 test_ip_filter(),
10685 Arc::new(Vec::new()),
10686 None,
10687 None,
10688 Arc::new(crate::transport::NetworkFactory::tokio()),
10689 None, Arc::new(crate::stats::SessionCounters::new()),
10691 )
10692 .await
10693 .unwrap();
10694
10695 let progress = handle.file_progress().await.unwrap();
10696 assert_eq!(
10697 progress.len(),
10698 3,
10699 "multi-file torrent should have 3 entries"
10700 );
10701 assert!(
10702 progress.iter().all(|&b| b == 0),
10703 "all progress should be 0 initially"
10704 );
10705
10706 handle.shutdown().await.unwrap();
10707 }
10708
10709 #[tokio::test]
10712 async fn is_valid_true_for_active() {
10713 let data = vec![0xAB; 32768];
10714 let meta = make_test_torrent(&data, 16384);
10715 let storage = make_storage(&data, 16384);
10716 let config = test_config();
10717
10718 let (atx, amask) = test_alert_channel();
10719 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10720 let handle = TorrentHandle::from_torrent(
10721 meta,
10722 irontide_core::TorrentVersion::V1Only,
10723 None,
10724 dh,
10725 dm,
10726 config,
10727 test_dht_rx(),
10728 test_dht_rx(),
10729 None,
10730 None,
10731 crate::slot_tuner::SlotTuner::disabled(4),
10732 atx,
10733 amask,
10734 None,
10735 None,
10736 test_ban_manager(),
10737 test_ip_filter(),
10738 Arc::new(Vec::new()),
10739 None,
10740 None,
10741 Arc::new(crate::transport::NetworkFactory::tokio()),
10742 None, Arc::new(crate::stats::SessionCounters::new()),
10744 )
10745 .await
10746 .unwrap();
10747
10748 assert!(
10749 handle.is_valid(),
10750 "handle should be valid while torrent actor is alive"
10751 );
10752
10753 handle.shutdown().await.unwrap();
10754 }
10755
10756 #[tokio::test]
10759 async fn is_valid_false_after_remove() {
10760 let data = vec![0xAB; 32768];
10761 let meta = make_test_torrent(&data, 16384);
10762 let storage = make_storage(&data, 16384);
10763 let config = test_config();
10764
10765 let (atx, amask) = test_alert_channel();
10766 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10767 let handle = TorrentHandle::from_torrent(
10768 meta,
10769 irontide_core::TorrentVersion::V1Only,
10770 None,
10771 dh,
10772 dm,
10773 config,
10774 test_dht_rx(),
10775 test_dht_rx(),
10776 None,
10777 None,
10778 crate::slot_tuner::SlotTuner::disabled(4),
10779 atx,
10780 amask,
10781 None,
10782 None,
10783 test_ban_manager(),
10784 test_ip_filter(),
10785 Arc::new(Vec::new()),
10786 None,
10787 None,
10788 Arc::new(crate::transport::NetworkFactory::tokio()),
10789 None, Arc::new(crate::stats::SessionCounters::new()),
10791 )
10792 .await
10793 .unwrap();
10794
10795 assert!(handle.is_valid());
10796
10797 handle.shutdown().await.unwrap();
10799
10800 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
10802
10803 assert!(
10804 !handle.is_valid(),
10805 "handle should be invalid after shutdown"
10806 );
10807 }
10808
10809 #[tokio::test]
10812 async fn clear_error_resets() {
10813 let data = vec![0xAB; 32768];
10814 let meta = make_test_torrent(&data, 16384);
10815 let storage = make_storage(&data, 16384);
10816 let config = test_config();
10817
10818 let (atx, amask) = test_alert_channel();
10819 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10820 let handle = TorrentHandle::from_torrent(
10821 meta,
10822 irontide_core::TorrentVersion::V1Only,
10823 None,
10824 dh,
10825 dm,
10826 config,
10827 test_dht_rx(),
10828 test_dht_rx(),
10829 None,
10830 None,
10831 crate::slot_tuner::SlotTuner::disabled(4),
10832 atx,
10833 amask,
10834 None,
10835 None,
10836 test_ban_manager(),
10837 test_ip_filter(),
10838 Arc::new(Vec::new()),
10839 None,
10840 None,
10841 Arc::new(crate::transport::NetworkFactory::tokio()),
10842 None, Arc::new(crate::stats::SessionCounters::new()),
10844 )
10845 .await
10846 .unwrap();
10847
10848 let stats = handle.stats().await.unwrap();
10850 assert!(stats.error.is_empty());
10851 assert_eq!(stats.error_file, -1);
10852
10853 handle.clear_error().await.unwrap();
10855
10856 let stats = handle.stats().await.unwrap();
10857 assert!(stats.error.is_empty());
10858 assert_eq!(stats.error_file, -1);
10859
10860 handle.shutdown().await.unwrap();
10861 }
10862
10863 #[tokio::test]
10866 async fn flags_round_trip() {
10867 let data = vec![0xAB; 32768];
10868 let meta = make_test_torrent(&data, 16384);
10869 let storage = make_storage(&data, 16384);
10870 let config = test_config();
10871
10872 let (atx, amask) = test_alert_channel();
10873 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10874 let handle = TorrentHandle::from_torrent(
10875 meta,
10876 irontide_core::TorrentVersion::V1Only,
10877 None,
10878 dh,
10879 dm,
10880 config,
10881 test_dht_rx(),
10882 test_dht_rx(),
10883 None,
10884 None,
10885 crate::slot_tuner::SlotTuner::disabled(4),
10886 atx,
10887 amask,
10888 None,
10889 None,
10890 test_ban_manager(),
10891 test_ip_filter(),
10892 Arc::new(Vec::new()),
10893 None,
10894 None,
10895 Arc::new(crate::transport::NetworkFactory::tokio()),
10896 None, Arc::new(crate::stats::SessionCounters::new()),
10898 )
10899 .await
10900 .unwrap();
10901
10902 let initial = handle.flags().await.unwrap();
10904 assert!(!initial.contains(crate::types::TorrentFlags::PAUSED));
10905 assert!(!initial.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10906 assert!(!initial.contains(crate::types::TorrentFlags::SUPER_SEEDING));
10907
10908 handle
10910 .set_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
10911 .await
10912 .unwrap();
10913 let after_set = handle.flags().await.unwrap();
10914 assert!(after_set.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10915
10916 handle
10918 .unset_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
10919 .await
10920 .unwrap();
10921 let after_unset = handle.flags().await.unwrap();
10922 assert!(!after_unset.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10923
10924 assert!(!handle.is_sequential_download().await.unwrap());
10926
10927 handle.shutdown().await.unwrap();
10928 }
10929
10930 #[tokio::test]
10933 async fn connect_peer_no_error() {
10934 let data = vec![0xAB; 32768];
10935 let meta = make_test_torrent(&data, 16384);
10936 let storage = make_storage(&data, 16384);
10937 let config = test_config();
10938
10939 let (atx, amask) = test_alert_channel();
10940 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10941 let handle = TorrentHandle::from_torrent(
10942 meta,
10943 irontide_core::TorrentVersion::V1Only,
10944 None,
10945 dh,
10946 dm,
10947 config,
10948 test_dht_rx(),
10949 test_dht_rx(),
10950 None,
10951 None,
10952 crate::slot_tuner::SlotTuner::disabled(4),
10953 atx,
10954 amask,
10955 None,
10956 None,
10957 test_ban_manager(),
10958 test_ip_filter(),
10959 Arc::new(Vec::new()),
10960 None,
10961 None,
10962 Arc::new(crate::transport::NetworkFactory::tokio()),
10963 None, Arc::new(crate::stats::SessionCounters::new()),
10965 )
10966 .await
10967 .unwrap();
10968
10969 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
10972 handle.connect_peer(addr).await.unwrap();
10973
10974 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
10976
10977 handle.shutdown().await.unwrap();
10978 }
10979
10980 fn make_test_meta_v2(
10984 piece_hashes: &[irontide_core::Id32],
10985 file_root: irontide_core::Id32,
10986 piece_length: u64,
10987 file_length: u64,
10988 ) -> irontide_core::TorrentMetaV2 {
10989 use std::collections::BTreeMap;
10990
10991 let mut layer_bytes = Vec::with_capacity(piece_hashes.len() * 32);
10993 for h in piece_hashes {
10994 layer_bytes.extend_from_slice(&h.0);
10995 }
10996
10997 let mut piece_layers = BTreeMap::new();
10998 piece_layers.insert(file_root, layer_bytes);
10999
11000 let file_tree = irontide_core::FileTreeNode::Directory({
11001 let mut children = BTreeMap::new();
11002 children.insert(
11003 "test.dat".to_string(),
11004 irontide_core::FileTreeNode::File(irontide_core::V2FileAttr {
11005 length: file_length,
11006 pieces_root: Some(file_root),
11007 }),
11008 );
11009 children
11010 });
11011
11012 irontide_core::TorrentMetaV2 {
11013 info_hashes: irontide_core::InfoHashes::v2_only(irontide_core::Id32::ZERO),
11014 info_bytes: None,
11015 announce: None,
11016 announce_list: None,
11017 comment: None,
11018 created_by: None,
11019 creation_date: None,
11020 info: irontide_core::InfoDictV2 {
11021 name: "test".to_string(),
11022 piece_length,
11023 meta_version: 2,
11024 file_tree,
11025 ssl_cert: None,
11026 },
11027 piece_layers,
11028 ssl_cert: None,
11029 }
11030 }
11031
11032 #[test]
11033 fn test_serve_hashes_v2_piece_layer() {
11034 let hashes: Vec<irontide_core::Id32> = (0..4u8)
11037 .map(|i| {
11038 let mut h = [0u8; 32];
11039 h[0] = i;
11040 irontide_core::Id32(h)
11041 })
11042 .collect();
11043 let file_root = irontide_core::Id32([0xAA; 32]);
11044 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11045 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11046
11047 let request = irontide_core::HashRequest {
11048 file_root,
11049 base: 0, index: 0,
11051 count: 4,
11052 proof_layers: 0,
11053 };
11054
11055 let result = serve_hashes(
11056 Some(&meta),
11057 irontide_core::TorrentVersion::V2Only,
11058 Some(&lengths),
11059 &request,
11060 );
11061 let served = result.expect("should serve hashes");
11062 assert_eq!(served.len(), 4);
11063 for (i, h) in served.iter().enumerate() {
11064 assert_eq!(h.0[0], i as u8);
11065 }
11066 }
11067
11068 #[test]
11069 fn test_serve_hashes_rejects_v1_only() {
11070 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
11071 let file_root = irontide_core::Id32([0xAA; 32]);
11072 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
11073 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
11074
11075 let request = irontide_core::HashRequest {
11076 file_root,
11077 base: 0,
11078 index: 0,
11079 count: 1,
11080 proof_layers: 0,
11081 };
11082
11083 let result = serve_hashes(
11084 Some(&meta),
11085 irontide_core::TorrentVersion::V1Only,
11086 Some(&lengths),
11087 &request,
11088 );
11089 assert!(result.is_none(), "V1Only should reject hash requests");
11090 }
11091
11092 #[test]
11093 fn test_serve_hashes_rejects_unknown_root() {
11094 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
11095 let file_root = irontide_core::Id32([0xAA; 32]);
11096 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
11097 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
11098
11099 let unknown_root = irontide_core::Id32([0xFF; 32]);
11101 let request = irontide_core::HashRequest {
11102 file_root: unknown_root,
11103 base: 0,
11104 index: 0,
11105 count: 1,
11106 proof_layers: 0,
11107 };
11108
11109 let result = serve_hashes(
11110 Some(&meta),
11111 irontide_core::TorrentVersion::V2Only,
11112 Some(&lengths),
11113 &request,
11114 );
11115 assert!(result.is_none(), "unknown file_root should reject");
11116 }
11117
11118 #[test]
11119 fn test_serve_hashes_rejects_out_of_bounds() {
11120 let hashes: Vec<irontide_core::Id32> =
11122 (0..2u8).map(|i| irontide_core::Id32([i; 32])).collect();
11123 let file_root = irontide_core::Id32([0xAA; 32]);
11124 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 2);
11125 let lengths = Lengths::new(16384 * 2, 16384, DEFAULT_CHUNK_SIZE);
11126
11127 let request = irontide_core::HashRequest {
11129 file_root,
11130 base: 0,
11131 index: 5,
11132 count: 1,
11133 proof_layers: 0,
11134 };
11135
11136 let result = serve_hashes(
11137 Some(&meta),
11138 irontide_core::TorrentVersion::V2Only,
11139 Some(&lengths),
11140 &request,
11141 );
11142 assert!(result.is_none(), "out-of-bounds index should reject");
11143 }
11144
11145 #[test]
11146 fn test_serve_hashes_includes_proofs() {
11147 let hashes: Vec<irontide_core::Id32> =
11150 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
11151 let file_root = irontide_core::Id32([0xAA; 32]);
11152 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11153 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11154
11155 let request = irontide_core::HashRequest {
11157 file_root,
11158 base: 0,
11159 index: 0,
11160 count: 1,
11161 proof_layers: 1,
11162 };
11163
11164 let result = serve_hashes(
11165 Some(&meta),
11166 irontide_core::TorrentVersion::V2Only,
11167 Some(&lengths),
11168 &request,
11169 );
11170 let served = result.expect("should serve hashes with proofs");
11171 assert_eq!(served.len(), 2, "should have 1 data hash + 1 proof hash");
11173 assert_eq!(served[0], hashes[0]);
11175 assert_eq!(served[1], hashes[1]);
11177 }
11178
11179 #[test]
11180 fn test_serve_hashes_proof_with_batch() {
11181 let hashes: Vec<irontide_core::Id32> =
11196 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
11197 let file_root = irontide_core::Id32([0xAA; 32]);
11198 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11199 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11200
11201 let request = irontide_core::HashRequest {
11202 file_root,
11203 base: 0,
11204 index: 0,
11205 count: 2,
11206 proof_layers: 1,
11207 };
11208
11209 let result = serve_hashes(
11210 Some(&meta),
11211 irontide_core::TorrentVersion::V2Only,
11212 Some(&lengths),
11213 &request,
11214 );
11215 let served = result.expect("should serve hashes with batch proof");
11216 assert_eq!(served.len(), 3, "should have 2 data hashes + 1 uncle hash");
11218 assert_eq!(served[0], hashes[0]);
11220 assert_eq!(served[1], hashes[1]);
11221 let tree = irontide_core::MerkleTree::from_leaves(&hashes);
11224 let expected_uncle = tree.layer(1)[1]; assert_eq!(served[2], expected_uncle);
11226
11227 let sub_root = irontide_core::MerkleTree::root_from_hashes(&served[..2]);
11230 let uncle_hashes = &served[2..];
11231 let leaf_index = request.index as usize / 2; assert!(
11233 irontide_core::MerkleTree::verify_proof(
11234 tree.root(),
11235 sub_root,
11236 leaf_index,
11237 uncle_hashes
11238 ),
11239 "subtree proof should verify against tree root"
11240 );
11241 }
11242
11243 #[test]
11244 fn is_i2p_synthetic_addr_detects_240_range() {
11245 assert!(is_i2p_synthetic_addr(&"240.0.0.1:1".parse().unwrap()));
11246 assert!(is_i2p_synthetic_addr(
11247 &"255.255.255.255:65535".parse().unwrap()
11248 ));
11249 assert!(!is_i2p_synthetic_addr(&"192.168.1.1:6881".parse().unwrap()));
11250 assert!(!is_i2p_synthetic_addr(&"[::1]:6881".parse().unwrap()));
11251 }
11252
11253 #[test]
11254 fn v6_retry_delay_progression() {
11255 let expected_ms = [100, 200, 400, 800, 1600, 3200, 5000, 5000, 5000, 5000, 5000];
11257 for (count, &expected) in expected_ms.iter().enumerate() {
11258 let delay_ms = {
11259 let base_ms: u64 = 100;
11260 let max_ms: u64 = 5000;
11261 base_ms
11262 .saturating_mul(1u64.checked_shl(count as u32).unwrap_or(u64::MAX))
11263 .min(max_ms)
11264 };
11265 assert_eq!(
11266 delay_ms, expected,
11267 "count={count}: expected {expected}ms, got {delay_ms}ms"
11268 );
11269 }
11270 }
11271
11272 #[test]
11275 fn peer_backoff_exponential() {
11276 let expected_ms: Vec<u64> = vec![400, 800, 1600, 3200, 6400, 12800, 25600, 30000, 30000];
11279 for (i, &expected) in expected_ms.iter().enumerate() {
11280 let attempt = (i as u32) + 1; let delay_ms = 200u64.saturating_mul(1u64 << attempt.min(10)).min(30_000);
11282 assert_eq!(
11283 delay_ms, expected,
11284 "attempt={attempt}: expected {expected}ms, got {delay_ms}ms"
11285 );
11286 }
11287 }
11288
11289 #[test]
11290 fn peer_backoff_clears_on_data() {
11291 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
11294 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
11295
11296 assert!(!backoff.contains_key(&addr));
11298
11299 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
11301 let next = attempt.saturating_add(1);
11302 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
11303 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
11304 backoff.insert(addr, (earliest, next));
11305 assert_eq!(backoff.get(&addr).unwrap().1, 1);
11306
11307 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
11309 let next = attempt.saturating_add(1);
11310 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
11311 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
11312 backoff.insert(addr, (earliest, next));
11313 assert_eq!(backoff.get(&addr).unwrap().1, 2);
11314
11315 backoff.remove(&addr);
11317 assert!(!backoff.contains_key(&addr));
11318 }
11319
11320 #[test]
11321 fn backoff_prevents_hammering() {
11322 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
11324 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
11325
11326 let future = std::time::Instant::now() + Duration::from_secs(10);
11328 backoff.insert(addr, (future, 3));
11329
11330 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
11332 assert!(std::time::Instant::now() < next_attempt);
11333 }
11334
11335 let past = std::time::Instant::now() - Duration::from_secs(1);
11337 backoff.insert(addr, (past, 3));
11338 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
11339 assert!(std::time::Instant::now() >= next_attempt);
11340 }
11341 }
11342
11343 #[test]
11344 fn max_in_flight_formula_updated() {
11345 let formula = |connected: usize, num_pieces: u32| -> usize {
11347 let calculated = 512usize.max(connected.saturating_mul(4));
11348 calculated.min(num_pieces as usize / 2).max(512)
11349 };
11350
11351 assert_eq!(formula(10, 2000), 512);
11353
11354 assert_eq!(formula(200, 2000), 800);
11356
11357 assert_eq!(formula(500, 2000), 1000); assert_eq!(formula(200, 100), 512); assert_eq!(formula(128, 10000), 512); assert_eq!(formula(129, 10000), 516); assert_eq!(formula(0, 10000), 512);
11369
11370 assert_eq!(formula(100, 0), 512);
11372 }
11373
11374 #[test]
11377 fn should_attempt_holepunch_reason_classification() {
11378 assert!(should_attempt_holepunch("connection refused"));
11380 assert!(should_attempt_holepunch("Connection refused"));
11381 assert!(should_attempt_holepunch("timed out"));
11382 assert!(should_attempt_holepunch("Connection reset by peer"));
11383 assert!(should_attempt_holepunch("connection reset by peer"));
11384 assert!(!should_attempt_holepunch(
11386 "holepunch TCP connect failed: Connection refused"
11387 ));
11388 assert!(!should_attempt_holepunch("peer banned"));
11390 assert!(!should_attempt_holepunch("protocol error"));
11391 assert!(!should_attempt_holepunch(""));
11392 }
11393
11394 #[test]
11395 fn holepunch_initiation_on_connect_failure() {
11396 assert!(should_attempt_holepunch("connection refused"));
11398 }
11399
11400 #[test]
11401 fn holepunch_cooldown_prevents_retry() {
11402 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
11403 let addr: SocketAddr = "127.0.0.1:6881".parse().expect("valid test addr");
11404 let now = Instant::now();
11405 cooldowns.insert(addr, now);
11406 assert!(cooldowns.contains_key(&addr));
11408 }
11409
11410 #[test]
11411 fn holepunch_cooldown_overflow_skips() {
11412 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
11413 let now = Instant::now();
11414 for i in 0..256u16 {
11415 let addr: SocketAddr = format!("10.0.{}.{}:6881", i / 256, i % 256)
11416 .parse()
11417 .expect("valid test addr");
11418 cooldowns.insert(addr, now);
11419 }
11420 assert_eq!(cooldowns.len(), HOLEPUNCH_MAX_TRACKED);
11421 }
11423
11424 #[test]
11425 fn holepunch_skipped_when_disabled() {
11426 assert!(should_attempt_holepunch("connection refused"));
11429 }
11431
11432 #[test]
11433 fn holepunch_not_triggered_on_ban() {
11434 assert!(!should_attempt_holepunch("peer banned"));
11435 assert!(!should_attempt_holepunch("banned for bad data"));
11436 }
11437
11438 fn make_multi_file_meta(files: &[(u64, &str)], piece_length: u64) -> TorrentMetaV1 {
11442 let total_length: u64 = files.iter().map(|(len, _)| *len).sum();
11443 let num_pieces = total_length.div_ceil(piece_length) as usize;
11444 let file_entries: Vec<irontide_core::FileEntry> = files
11445 .iter()
11446 .map(|(length, name)| irontide_core::FileEntry {
11447 length: *length,
11448 path: vec![name.to_string()],
11449 attr: None,
11450 mtime: None,
11451 symlink_path: None,
11452 })
11453 .collect();
11454 TorrentMetaV1 {
11455 info_hash: Id20([0u8; 20]),
11456 announce: None,
11457 announce_list: None,
11458 comment: None,
11459 created_by: None,
11460 creation_date: None,
11461 info: irontide_core::InfoDict {
11462 name: "test".to_string(),
11463 piece_length,
11464 pieces: vec![0u8; num_pieces * 20],
11465 length: None,
11466 files: Some(file_entries),
11467 private: None,
11468 source: None,
11469 ssl_cert: None,
11470 similar: Vec::new(),
11471 collections: Vec::new(),
11472 },
11473 url_list: Vec::new(),
11474 httpseeds: Vec::new(),
11475 info_bytes: None,
11476 ssl_cert: None,
11477 }
11478 }
11479
11480 #[test]
11481 fn cached_files_populated_on_registration() {
11482 let meta = make_multi_file_meta(&[(100, "a.txt"), (200, "b.txt"), (50, "c.txt")], 100);
11488 let lengths = Lengths::new(350, 100, 16384);
11489 let cached = build_cached_file_info(&meta, &lengths);
11490
11491 assert_eq!(cached.entries.len(), 3);
11492
11493 assert_eq!(cached.entries[0].index, 0);
11494 assert_eq!(cached.entries[0].length, 100);
11495 assert_eq!(cached.entries[0].first_piece, 0);
11496 assert_eq!(cached.entries[0].last_piece, 0);
11497
11498 assert_eq!(cached.entries[1].index, 1);
11499 assert_eq!(cached.entries[1].length, 200);
11500 assert_eq!(cached.entries[1].first_piece, 1);
11501 assert_eq!(cached.entries[1].last_piece, 2);
11502
11503 assert_eq!(cached.entries[2].index, 2);
11504 assert_eq!(cached.entries[2].length, 50);
11505 assert_eq!(cached.entries[2].first_piece, 3);
11506 assert_eq!(cached.entries[2].last_piece, 3);
11507 }
11508
11509 #[test]
11510 fn cached_files_single_file_torrent() {
11511 let meta = TorrentMetaV1 {
11514 info_hash: Id20([0u8; 20]),
11515 announce: None,
11516 announce_list: None,
11517 comment: None,
11518 created_by: None,
11519 creation_date: None,
11520 info: irontide_core::InfoDict {
11521 name: "single.bin".to_string(),
11522 piece_length: 100,
11523 pieces: vec![0u8; 5 * 20],
11524 length: Some(500),
11525 files: None,
11526 private: None,
11527 source: None,
11528 ssl_cert: None,
11529 similar: Vec::new(),
11530 collections: Vec::new(),
11531 },
11532 url_list: Vec::new(),
11533 httpseeds: Vec::new(),
11534 info_bytes: None,
11535 ssl_cert: None,
11536 };
11537 let lengths = Lengths::new(500, 100, 16384);
11538 let cached = build_cached_file_info(&meta, &lengths);
11539
11540 assert_eq!(cached.entries.len(), 1);
11541 assert_eq!(cached.entries[0].index, 0);
11542 assert_eq!(cached.entries[0].length, 500);
11543 assert_eq!(cached.entries[0].first_piece, 0);
11544 assert_eq!(cached.entries[0].last_piece, 4);
11545 }
11546
11547 use crate::piece_reservation::{AtomicPieceStates, PieceState, StealCandidates};
11554 use irontide_storage::Bitfield;
11555
11556 fn steal_populate_scan(states: &AtomicPieceStates, sc: &StealCandidates) -> u32 {
11560 let mut pushed = 0u32;
11561 let num = states.len();
11562 for piece in 0..num {
11563 let state = states.get(piece);
11564 if state == PieceState::Reserved {
11565 sc.push(piece);
11566 pushed = pushed.saturating_add(1);
11567 }
11568 }
11569 pushed
11570 }
11571
11572 fn all_wanted(n: u32) -> Bitfield {
11573 let mut bf = Bitfield::new(n);
11574 for i in 0..n {
11575 bf.set(i);
11576 }
11577 bf
11578 }
11579
11580 #[test]
11581 fn steal_populate_pushes_reserved_pieces() {
11582 let n = 10;
11583 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11584 let sc = StealCandidates::new();
11585
11586 assert!(states.try_reserve(2));
11588 assert!(states.try_reserve(5));
11589 assert!(states.try_reserve(7));
11590
11591 let pushed = steal_populate_scan(&states, &sc);
11592 assert_eq!(pushed, 3, "should push exactly the 3 reserved pieces");
11593
11594 let mut popped = Vec::new();
11596 while let Some(p) = sc.pop() {
11597 popped.push(p);
11598 }
11599 popped.sort_unstable();
11600 assert_eq!(popped, vec![2, 5, 7]);
11601 }
11602
11603 #[test]
11604 fn steal_populate_skips_non_reserved_states() {
11605 let n = 8;
11606 let mut have = Bitfield::new(n);
11607 have.set(0); let mut wanted = all_wanted(n);
11609 wanted.clear(1); let states = AtomicPieceStates::new(n, &have, &wanted);
11612 let sc = StealCandidates::new();
11613
11614 assert!(states.try_reserve(3));
11616
11617 let pushed = steal_populate_scan(&states, &sc);
11618 assert_eq!(pushed, 1, "only piece 3 (Reserved) should be pushed");
11619
11620 assert_eq!(sc.pop(), Some(3));
11621 assert_eq!(sc.pop(), None);
11622 }
11623
11624 #[test]
11625 fn steal_populate_deduplicates() {
11626 let n = 4;
11627 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11628 let sc = StealCandidates::new();
11629
11630 assert!(states.try_reserve(1));
11631 assert!(states.try_reserve(2));
11632
11633 let pushed1 = steal_populate_scan(&states, &sc);
11635 assert_eq!(pushed1, 2);
11636
11637 let pushed2 = steal_populate_scan(&states, &sc);
11640 assert_eq!(pushed2, 2, "scan still reports 2 reserved pieces");
11641
11642 let mut count = 0u32;
11643 while sc.pop().is_some() {
11644 count = count.saturating_add(1);
11645 }
11646 assert_eq!(count, 2, "dedup means only 2 entries despite 2 scans");
11647 }
11648
11649 #[test]
11650 fn steal_populate_skips_completed_pieces() {
11651 let n = 5;
11652 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11653 let sc = StealCandidates::new();
11654
11655 for i in 0..n {
11657 assert!(states.try_reserve(i));
11658 }
11659
11660 states.mark_complete(1);
11662 states.mark_complete(3);
11663
11664 let pushed = steal_populate_scan(&states, &sc);
11665 assert_eq!(pushed, 3, "3 pieces still Reserved (0, 2, 4)");
11666
11667 let mut popped = Vec::new();
11668 while let Some(p) = sc.pop() {
11669 popped.push(p);
11670 }
11671 popped.sort_unstable();
11672 assert_eq!(popped, vec![0, 2, 4]);
11673 }
11674
11675 #[test]
11676 fn steal_populate_empty_when_no_reserved() {
11677 let n = 6;
11678 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11679 let sc = StealCandidates::new();
11680
11681 let pushed = steal_populate_scan(&states, &sc);
11683 assert_eq!(pushed, 0);
11684 assert_eq!(sc.pop(), None);
11685 }
11686
11687 #[test]
11688 fn steal_populate_with_endgame_pieces() {
11689 let n = 4;
11691 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11692 let sc = StealCandidates::new();
11693
11694 assert!(states.try_reserve(0));
11695 assert!(states.try_reserve(1));
11696 states.transition_to_endgame(1);
11697
11698 let pushed = steal_populate_scan(&states, &sc);
11699 assert_eq!(
11700 pushed, 1,
11701 "only piece 0 (Reserved) should be pushed, not piece 1 (Endgame)"
11702 );
11703 assert_eq!(sc.pop(), Some(0));
11704 assert_eq!(sc.pop(), None);
11705 }
11706
11707 #[test]
11712 fn sync_piece_states_marks_unwanted_on_skip() {
11713 let n = 8;
11714 let mut wanted = all_wanted(n);
11715 wanted.clear(2);
11716 wanted.clear(3);
11717 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11718 assert_eq!(states.get(2), PieceState::Available);
11721 assert_eq!(states.get(3), PieceState::Available);
11722
11723 for piece in 0..n {
11725 let w = wanted.get(piece);
11726 let current = states.get(piece);
11727 if !w && current == PieceState::Available {
11728 states.mark_unwanted(piece);
11729 } else if w && current == PieceState::Unwanted {
11730 states.mark_available(piece);
11731 }
11732 }
11733
11734 assert_eq!(states.get(0), PieceState::Available);
11735 assert_eq!(states.get(2), PieceState::Unwanted);
11736 assert_eq!(states.get(3), PieceState::Unwanted);
11737 assert_eq!(states.get(4), PieceState::Available);
11738 }
11739
11740 #[test]
11741 fn sync_piece_states_restores_available_on_unskip() {
11742 let n = 6;
11743 let mut initial_wanted = all_wanted(n);
11744 initial_wanted.clear(1);
11745 initial_wanted.clear(4);
11746 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &initial_wanted);
11747 assert_eq!(states.get(1), PieceState::Unwanted);
11748 assert_eq!(states.get(4), PieceState::Unwanted);
11749
11750 let new_wanted = all_wanted(n);
11752 for piece in 0..n {
11753 let w = new_wanted.get(piece);
11754 let current = states.get(piece);
11755 if !w && current == PieceState::Available {
11756 states.mark_unwanted(piece);
11757 } else if w && current == PieceState::Unwanted {
11758 states.mark_available(piece);
11759 }
11760 }
11761
11762 assert_eq!(states.get(1), PieceState::Available);
11763 assert_eq!(states.get(4), PieceState::Available);
11764 }
11765
11766 #[test]
11767 fn sync_piece_states_shared_piece_stays_available() {
11768 let n = 4;
11772 let mut wanted = all_wanted(n);
11773 wanted.clear(0); let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11776
11777 for piece in 0..n {
11778 let w = wanted.get(piece);
11779 let current = states.get(piece);
11780 if !w && current == PieceState::Available {
11781 states.mark_unwanted(piece);
11782 } else if w && current == PieceState::Unwanted {
11783 states.mark_available(piece);
11784 }
11785 }
11786
11787 assert_eq!(states.get(0), PieceState::Unwanted);
11788 assert_eq!(
11789 states.get(1),
11790 PieceState::Available,
11791 "shared piece stays Available"
11792 );
11793 assert_eq!(states.get(2), PieceState::Available);
11794 assert_eq!(states.get(3), PieceState::Available);
11795 }
11796
11797 #[test]
11806 fn dht_requery_guard_scales_with_max_peers() {
11807 assert_eq!(128_usize.saturating_mul(4), 512);
11809
11810 assert_eq!(200_usize.saturating_mul(4), 800);
11812
11813 assert_eq!(50_usize.saturating_mul(4), 200);
11815
11816 assert_eq!(usize::MAX.saturating_mul(4), usize::MAX);
11818 }
11819
11820 fn make_test_info_bytes() -> (Vec<u8>, Id20) {
11824 use serde::Serialize;
11825
11826 #[derive(Serialize)]
11827 struct Info<'a> {
11828 length: u64,
11829 name: &'a str,
11830 #[serde(rename = "piece length")]
11831 piece_length: u64,
11832 #[serde(with = "serde_bytes")]
11833 pieces: &'a [u8],
11834 }
11835
11836 let data = vec![0xAB; 1024];
11837 let piece_hash = irontide_core::sha1(&data);
11838 let mut pieces = Vec::new();
11839 pieces.extend_from_slice(piece_hash.as_bytes());
11840
11841 let info = Info {
11842 length: 1024,
11843 name: "test",
11844 piece_length: 16384,
11845 pieces: &pieces,
11846 };
11847
11848 let info_bytes = irontide_bencode::to_bytes(&info).unwrap();
11849 let info_hash = irontide_core::sha1(&info_bytes);
11850 (info_bytes, info_hash)
11851 }
11852
11853 async fn create_magnet_handle(info_hash: Id20) -> TorrentHandle {
11855 let magnet = Magnet {
11856 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
11857 display_name: Some("test".into()),
11858 trackers: vec![],
11859 peers: vec![],
11860 selected_files: None,
11861 };
11862 let config = test_config();
11863 let (atx, amask) = test_alert_channel();
11864 let (dm, _dj) = test_disk_manager();
11865 TorrentHandle::from_magnet(
11866 magnet,
11867 dm,
11868 config,
11869 test_dht_rx(),
11870 test_dht_rx(),
11871 None,
11872 None,
11873 crate::slot_tuner::SlotTuner::disabled(4),
11874 atx,
11875 amask,
11876 None,
11877 None,
11878 test_ban_manager(),
11879 test_ip_filter(),
11880 Arc::new(Vec::new()),
11881 None,
11882 None,
11883 Arc::new(crate::transport::NetworkFactory::tokio()),
11884 None,
11885 Arc::new(crate::stats::SessionCounters::new()),
11886 )
11887 .await
11888 .unwrap()
11889 }
11890
11891 #[tokio::test]
11892 async fn pre_resolved_metadata_applies_when_fetching() {
11893 let (info_bytes, info_hash) = make_test_info_bytes();
11894 let handle = create_magnet_handle(info_hash).await;
11895
11896 let stats = handle.stats().await.unwrap();
11898 assert_eq!(stats.state, TorrentState::FetchingMetadata);
11899
11900 let peer_addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
11902 handle.send_pre_resolved_metadata(info_bytes, vec![peer_addr]);
11903
11904 tokio::time::sleep(Duration::from_millis(200)).await;
11906
11907 let stats = handle.stats().await.unwrap();
11909 assert_eq!(
11910 stats.state,
11911 TorrentState::Downloading,
11912 "should have transitioned to Downloading after pre-resolved metadata"
11913 );
11914 assert!(
11915 stats.pieces_total > 0,
11916 "should know piece count after metadata resolution"
11917 );
11918
11919 handle.shutdown().await.unwrap();
11920 }
11921
11922 #[tokio::test]
11923 async fn pre_resolved_metadata_ignored_after_resolution() {
11924 let data = vec![0xAB; 32768];
11926 let meta = make_test_torrent(&data, 16384);
11927 let info_hash = meta.info_hash;
11928 let storage = make_storage(&data, 16384);
11929 let config = test_config();
11930
11931 let (atx, amask) = test_alert_channel();
11932 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
11933 let handle = TorrentHandle::from_torrent(
11934 meta,
11935 irontide_core::TorrentVersion::V1Only,
11936 None,
11937 dh,
11938 dm,
11939 config,
11940 test_dht_rx(),
11941 test_dht_rx(),
11942 None,
11943 None,
11944 crate::slot_tuner::SlotTuner::disabled(4),
11945 atx,
11946 amask,
11947 None,
11948 None,
11949 test_ban_manager(),
11950 test_ip_filter(),
11951 Arc::new(Vec::new()),
11952 None,
11953 None,
11954 Arc::new(crate::transport::NetworkFactory::tokio()),
11955 None,
11956 Arc::new(crate::stats::SessionCounters::new()),
11957 )
11958 .await
11959 .unwrap();
11960
11961 let stats_before = handle.stats().await.unwrap();
11962 assert_eq!(stats_before.state, TorrentState::Downloading);
11963
11964 let (info_bytes, _) = make_test_info_bytes();
11967 handle.send_pre_resolved_metadata(info_bytes, vec![]);
11968
11969 tokio::time::sleep(Duration::from_millis(100)).await;
11971
11972 let stats_after = handle.stats().await.unwrap();
11974 assert_eq!(stats_after.state, TorrentState::Downloading);
11975 assert_eq!(stats_after.pieces_total, stats_before.pieces_total);
11976
11977 handle.shutdown().await.unwrap();
11978 }
11979
11980 #[tokio::test]
11981 async fn pre_resolved_metadata_with_invalid_hash_stays_fetching() {
11982 let (info_bytes, _correct_hash) = make_test_info_bytes();
11986
11987 let wrong_hash = Id20::from_hex("0000000000000000000000000000000000000001").unwrap();
11989 let handle = create_magnet_handle(wrong_hash).await;
11990
11991 let stats = handle.stats().await.unwrap();
11992 assert_eq!(stats.state, TorrentState::FetchingMetadata);
11993
11994 handle.send_pre_resolved_metadata(info_bytes, vec![]);
11996
11997 tokio::time::sleep(Duration::from_millis(200)).await;
11998
11999 let stats = handle.stats().await.unwrap();
12001 assert_eq!(
12002 stats.state,
12003 TorrentState::FetchingMetadata,
12004 "should stay in FetchingMetadata when info_hash doesn't match"
12005 );
12006
12007 handle.shutdown().await.unwrap();
12008 }
12009
12010 #[test]
12011 fn initial_queue_depth_is_128() {
12012 use crate::peer_shared::INITIAL_QUEUE_DEPTH;
12013 assert_eq!(INITIAL_QUEUE_DEPTH, 128);
12014 }
12015
12016 #[tokio::test]
12029 #[allow(
12030 clippy::large_stack_arrays,
12031 reason = "test data buffer passed directly to make_storage"
12032 )]
12033 async fn m159_seed_mode_suppresses_new_requests_on_wire() {
12034 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let info_hash = meta.info_hash;
12037 let storage = make_storage(&[0u8; 32768], 16384);
12039
12040 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
12041 let listen_addr = listener.local_addr().unwrap();
12042 let config = TorrentConfig {
12043 listen_port: listen_addr.port(),
12044 ..test_config()
12045 };
12046 drop(listener);
12047
12048 let (atx, amask) = test_alert_channel();
12049 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
12050 let handle = TorrentHandle::from_torrent(
12051 meta,
12052 irontide_core::TorrentVersion::V1Only,
12053 None,
12054 dh,
12055 dm,
12056 config,
12057 test_dht_rx(),
12058 test_dht_rx(),
12059 None,
12060 None,
12061 crate::slot_tuner::SlotTuner::disabled(4),
12062 atx,
12063 amask,
12064 None,
12065 None,
12066 test_ban_manager(),
12067 test_ip_filter(),
12068 Arc::new(Vec::new()),
12069 None,
12070 None,
12071 Arc::new(crate::transport::NetworkFactory::tokio()),
12072 None,
12073 Arc::new(crate::stats::SessionCounters::new()),
12074 )
12075 .await
12076 .unwrap();
12077
12078 tokio::time::sleep(Duration::from_millis(50)).await;
12079
12080 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
12082 let (reader, writer) = tokio::io::split(stream);
12083 let mut writer = writer;
12084 let mut reader = reader;
12085
12086 let hs = Handshake::new(
12087 info_hash,
12088 Id20::from_hex("dddddddddddddddddddddddddddddddddddddddd").unwrap(),
12089 );
12090 writer.write_all(&hs.to_bytes()).await.unwrap();
12091 writer.flush().await.unwrap();
12092 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
12093 reader.read_exact(&mut hs_buf).await.unwrap();
12094
12095 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
12096 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
12097
12098 let _actor_ext_hs = framed_read.next().await;
12100 let ext_hs = ExtHandshake::new();
12101 let ext_payload = ext_hs.to_bytes().unwrap();
12102 framed_write
12103 .send(Message::Extended {
12104 ext_id: 0,
12105 payload: ext_payload,
12106 })
12107 .await
12108 .unwrap();
12109
12110 let mut bf = Bitfield::new(2);
12112 bf.set(0);
12113 bf.set(1);
12114 framed_write
12115 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
12116 .await
12117 .unwrap();
12118 framed_write.send(Message::Unchoke).await.unwrap();
12119
12120 let mut initial_request_seen = false;
12124 let wait_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12125 loop {
12126 let remaining = wait_deadline.saturating_duration_since(tokio::time::Instant::now());
12127 if remaining.is_zero() {
12128 break;
12129 }
12130 match tokio::time::timeout(remaining, framed_read.next()).await {
12131 Ok(Some(Ok(Message::Request { .. }))) => {
12132 initial_request_seen = true;
12133 break;
12134 }
12135 Ok(Some(Ok(_))) => {}
12136 _ => break,
12137 }
12138 }
12139 assert!(
12140 initial_request_seen,
12141 "actor should have sent a Request before seed mode toggle"
12142 );
12143
12144 handle.set_seed_mode(true).await.unwrap();
12147
12148 let grace_deadline = tokio::time::Instant::now() + Duration::from_millis(200);
12155 let mut cancel_seen = false;
12156 let mut grace_requests = 0u32;
12157 loop {
12158 let remaining = grace_deadline.saturating_duration_since(tokio::time::Instant::now());
12159 if remaining.is_zero() {
12160 break;
12161 }
12162 match tokio::time::timeout(remaining, framed_read.next()).await {
12163 Ok(Some(Ok(Message::Request { .. }))) => {
12164 grace_requests += 1;
12165 }
12166 Ok(Some(Ok(Message::Cancel { .. }))) => {
12167 cancel_seen = true;
12168 }
12169 Ok(Some(Ok(_))) => {}
12170 Ok(None | Some(Err(_))) | Err(_) => break,
12171 }
12172 }
12173 let _ = (cancel_seen, grace_requests);
12174
12175 let steady_deadline = tokio::time::Instant::now() + Duration::from_millis(500);
12178 let mut steady_requests = 0u32;
12179 loop {
12180 let remaining = steady_deadline.saturating_duration_since(tokio::time::Instant::now());
12181 if remaining.is_zero() {
12182 break;
12183 }
12184 match tokio::time::timeout(remaining, framed_read.next()).await {
12185 Ok(Some(Ok(Message::Request { .. }))) => {
12186 steady_requests += 1;
12187 }
12188 Ok(Some(Ok(_))) => {}
12189 Ok(None | Some(Err(_))) | Err(_) => break,
12190 }
12191 }
12192
12193 assert_eq!(
12194 steady_requests, 0,
12195 "after the Stop propagation grace window, no new Request messages \
12196 must appear during steady-state while user_seed_mode is active"
12197 );
12198
12199 let stats = handle.stats().await.unwrap();
12201 assert!(
12202 stats.user_seed_mode,
12203 "stats.user_seed_mode should be true after set_seed_mode(true)"
12204 );
12205
12206 handle.shutdown().await.unwrap();
12207 }
12208
12209 #[tokio::test]
12237 async fn m159_seed_mode_uploads_continue_on_wire() {
12238 const FILL_BYTE: u8 = 0x5A;
12239 const PIECE_LENGTH: u64 = 16384;
12240 const TOTAL_LEN: usize = 32768; let data = vec![FILL_BYTE; TOTAL_LEN];
12243 let meta = make_test_torrent(&data, PIECE_LENGTH);
12244 let info_hash = meta.info_hash;
12245 let storage = make_seeded_storage(&data, PIECE_LENGTH);
12247
12248 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
12249 let listen_addr = listener.local_addr().unwrap();
12250 let config = TorrentConfig {
12251 listen_port: listen_addr.port(),
12252 ..test_config()
12253 };
12254 drop(listener);
12255
12256 let (atx, amask) = test_alert_channel();
12257 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
12258 let handle = TorrentHandle::from_torrent(
12259 meta,
12260 irontide_core::TorrentVersion::V1Only,
12261 None,
12262 dh,
12263 dm,
12264 config,
12265 test_dht_rx(),
12266 test_dht_rx(),
12267 None,
12268 None,
12269 crate::slot_tuner::SlotTuner::disabled(4),
12270 atx,
12271 amask,
12272 None,
12273 None,
12274 test_ban_manager(),
12275 test_ip_filter(),
12276 Arc::new(Vec::new()),
12277 None,
12278 None,
12279 Arc::new(crate::transport::NetworkFactory::tokio()),
12280 None,
12281 Arc::new(crate::stats::SessionCounters::new()),
12282 )
12283 .await
12284 .unwrap();
12285
12286 let seeding_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12289 loop {
12290 tokio::time::sleep(Duration::from_millis(50)).await;
12291 let stats = handle.stats().await.unwrap();
12292 if stats.state == TorrentState::Seeding && stats.pieces_have == 2 {
12293 break;
12294 }
12295 if tokio::time::Instant::now() > seeding_deadline {
12296 let stats = handle.stats().await.unwrap();
12297 panic!(
12298 "actor did not reach Seeding state within 3s: state={:?}, have={}/{}",
12299 stats.state, stats.pieces_have, stats.pieces_total
12300 );
12301 }
12302 }
12303
12304 handle.set_seed_mode(true).await.unwrap();
12307 let stats = handle.stats().await.unwrap();
12308 assert!(
12309 stats.user_seed_mode,
12310 "stats.user_seed_mode should be true after set_seed_mode(true)"
12311 );
12312
12313 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
12315 let (reader, writer) = tokio::io::split(stream);
12316 let mut writer = writer;
12317 let mut reader = reader;
12318
12319 let hs = Handshake::new(
12320 info_hash,
12321 Id20::from_hex("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(),
12322 );
12323 writer.write_all(&hs.to_bytes()).await.unwrap();
12324 writer.flush().await.unwrap();
12325 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
12326 reader.read_exact(&mut hs_buf).await.unwrap();
12327
12328 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
12329 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
12330
12331 let _actor_ext_hs = framed_read.next().await;
12333 let ext_hs = ExtHandshake::new();
12334 let ext_payload = ext_hs.to_bytes().unwrap();
12335 framed_write
12336 .send(Message::Extended {
12337 ext_id: 0,
12338 payload: ext_payload,
12339 })
12340 .await
12341 .unwrap();
12342
12343 let bf = Bitfield::new(2);
12345 framed_write
12346 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
12347 .await
12348 .unwrap();
12349 framed_write.send(Message::Interested).await.unwrap();
12350
12351 let unchoke_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12355 let mut saw_unchoke = false;
12356 loop {
12357 let remaining = unchoke_deadline.saturating_duration_since(tokio::time::Instant::now());
12358 if remaining.is_zero() {
12359 break;
12360 }
12361 match tokio::time::timeout(remaining, framed_read.next()).await {
12362 Ok(Some(Ok(Message::Unchoke))) => {
12363 saw_unchoke = true;
12364 break;
12365 }
12366 Ok(Some(Ok(_))) => {}
12367 Ok(None | Some(Err(_))) => break,
12368 Err(_elapsed) => break,
12369 }
12370 }
12371 assert!(
12372 saw_unchoke,
12373 "actor should have unchoked the leecher while user_seed_mode is active"
12374 );
12375
12376 framed_write
12379 .send(Message::Request {
12380 index: 0,
12381 begin: 0,
12382 length: PIECE_LENGTH as u32,
12383 })
12384 .await
12385 .unwrap();
12386
12387 let piece_deadline = tokio::time::Instant::now() + Duration::from_secs(2);
12392 let mut got_piece = false;
12393 loop {
12394 let remaining = piece_deadline.saturating_duration_since(tokio::time::Instant::now());
12395 if remaining.is_zero() {
12396 break;
12397 }
12398 match tokio::time::timeout(remaining, framed_read.next()).await {
12399 Ok(Some(Ok(Message::Piece {
12400 index,
12401 begin,
12402 data_0,
12403 data_1,
12404 }))) => {
12405 assert_eq!(index, 0, "Piece index should match request");
12406 assert_eq!(begin, 0, "Piece begin should match request");
12407 let mut payload: Vec<u8> =
12408 Vec::with_capacity(data_0.len().saturating_add(data_1.len()));
12409 payload.extend_from_slice(&data_0);
12410 payload.extend_from_slice(&data_1);
12411 assert_eq!(
12412 payload.len(),
12413 PIECE_LENGTH as usize,
12414 "Piece payload length should match requested length"
12415 );
12416 assert!(
12417 payload.iter().all(|&b| b == FILL_BYTE),
12418 "Piece payload should contain the pre-seeded fill byte"
12419 );
12420 got_piece = true;
12421 break;
12422 }
12423 Ok(Some(Ok(_))) => {}
12424 Ok(None | Some(Err(_))) => break,
12425 Err(_elapsed) => break,
12426 }
12427 }
12428 assert!(
12429 got_piece,
12430 "actor should have served a Piece in response to Request while user_seed_mode is active"
12431 );
12432
12433 let stats = handle.stats().await.unwrap();
12436 assert!(
12437 stats.user_seed_mode,
12438 "stats.user_seed_mode should remain true after serving an upload"
12439 );
12440 assert!(
12441 stats.uploaded >= u64::from(PIECE_LENGTH as u32),
12442 "stats.uploaded should reflect the served block, got {}",
12443 stats.uploaded
12444 );
12445
12446 handle.shutdown().await.unwrap();
12447 }
12448
12449 #[tokio::test]
12452 async fn info_field_populated_for_torrent() {
12453 let data = vec![0xAB; 32768];
12454 let meta = make_test_torrent(&data, 16384);
12455 let storage = make_storage(&data, 16384);
12456 let config = test_config();
12457
12458 let (atx, amask) = test_alert_channel();
12459 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
12460 let handle = TorrentHandle::from_torrent(
12461 meta,
12462 irontide_core::TorrentVersion::V1Only,
12463 None,
12464 dh,
12465 dm,
12466 config,
12467 test_dht_rx(),
12468 test_dht_rx(),
12469 None,
12470 None,
12471 crate::slot_tuner::SlotTuner::disabled(4),
12472 atx,
12473 amask,
12474 None,
12475 None,
12476 test_ban_manager(),
12477 test_ip_filter(),
12478 Arc::new(Vec::new()),
12479 None,
12480 None,
12481 Arc::new(crate::transport::NetworkFactory::tokio()),
12482 None,
12483 Arc::new(crate::stats::SessionCounters::new()),
12484 )
12485 .await
12486 .unwrap();
12487
12488 tokio::time::sleep(Duration::from_millis(50)).await;
12489
12490 let rd = handle.save_resume_data().await.unwrap();
12491
12492 assert!(rd.info.is_some(), "rd.info should be Some for .torrent");
12494
12495 let info_bytes = rd.info.as_ref().unwrap();
12497 let info: irontide_core::InfoDict =
12498 irontide_bencode::from_bytes(info_bytes).expect("info bytes should deserialize");
12499 assert_eq!(info.name, "test");
12500 assert_eq!(info.piece_length, 16384);
12501
12502 handle.shutdown().await.unwrap();
12503 }
12504
12505 #[tokio::test]
12506 async fn info_hash2_none_for_v1_only() {
12507 let data = vec![0xCD; 16384];
12508 let meta = make_test_torrent(&data, 16384);
12509 let storage = make_storage(&data, 16384);
12510 let config = test_config();
12511
12512 let (atx, amask) = test_alert_channel();
12513 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
12514 let handle = TorrentHandle::from_torrent(
12515 meta,
12516 irontide_core::TorrentVersion::V1Only,
12517 None,
12518 dh,
12519 dm,
12520 config,
12521 test_dht_rx(),
12522 test_dht_rx(),
12523 None,
12524 None,
12525 crate::slot_tuner::SlotTuner::disabled(4),
12526 atx,
12527 amask,
12528 None,
12529 None,
12530 test_ban_manager(),
12531 test_ip_filter(),
12532 Arc::new(Vec::new()),
12533 None,
12534 None,
12535 Arc::new(crate::transport::NetworkFactory::tokio()),
12536 None,
12537 Arc::new(crate::stats::SessionCounters::new()),
12538 )
12539 .await
12540 .unwrap();
12541
12542 tokio::time::sleep(Duration::from_millis(50)).await;
12543
12544 let rd = handle.save_resume_data().await.unwrap();
12545
12546 assert!(
12548 rd.info_hash2.is_none(),
12549 "v1-only torrent should have info_hash2 = None"
12550 );
12551
12552 assert!(
12554 rd.added_time > 0,
12555 "added_time should be a positive POSIX timestamp"
12556 );
12557
12558 handle.shutdown().await.unwrap();
12559 }
12560
12561 #[tokio::test]
12562 async fn info_none_for_unresolved_magnet() {
12563 let magnet = Magnet {
12564 info_hashes: irontide_core::InfoHashes::v1_only(
12565 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
12566 ),
12567 display_name: Some("magnet-test".into()),
12568 trackers: vec![],
12569 peers: vec![],
12570 selected_files: None,
12571 };
12572 let config = test_config();
12573
12574 let (atx, amask) = test_alert_channel();
12575 let (dm, _dj) = test_disk_manager();
12576 let handle = TorrentHandle::from_magnet(
12577 magnet,
12578 dm,
12579 config,
12580 test_dht_rx(),
12581 test_dht_rx(),
12582 None,
12583 None,
12584 crate::slot_tuner::SlotTuner::disabled(4),
12585 atx,
12586 amask,
12587 None,
12588 None,
12589 test_ban_manager(),
12590 test_ip_filter(),
12591 Arc::new(Vec::new()),
12592 None,
12593 None,
12594 Arc::new(crate::transport::NetworkFactory::tokio()),
12595 None,
12596 Arc::new(crate::stats::SessionCounters::new()),
12597 )
12598 .await
12599 .unwrap();
12600
12601 tokio::time::sleep(Duration::from_millis(50)).await;
12602
12603 let rd = handle.save_resume_data().await.unwrap();
12604
12605 assert!(
12607 rd.info.is_none(),
12608 "unresolved magnet should have info = None"
12609 );
12610
12611 assert!(
12613 rd.added_time > 0,
12614 "added_time should be set for magnet links"
12615 );
12616
12617 handle.shutdown().await.unwrap();
12618 }
12619
12620 #[tokio::test]
12623 async fn torrent_command_get_meta_returns_none_before_metadata() {
12624 let info_hash =
12626 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").expect("valid hex");
12627 let handle = create_magnet_handle(info_hash).await;
12628
12629 let (tx, rx) = oneshot::channel();
12630 handle
12631 .cmd_tx
12632 .send(TorrentCommand::GetMeta { reply: tx })
12633 .await
12634 .expect("cmd_tx send");
12635 let result = rx.await.expect("GetMeta reply");
12636 assert!(
12637 result.is_none(),
12638 "pre-metadata magnet must return None from GetMeta"
12639 );
12640
12641 handle.shutdown().await.unwrap();
12642 }
12643
12644 #[tokio::test]
12645 async fn torrent_command_get_meta_returns_some_after_metadata() {
12646 let (info_bytes, info_hash) = make_test_info_bytes();
12649 let handle = create_magnet_handle(info_hash).await;
12650
12651 handle.send_pre_resolved_metadata(info_bytes, vec![]);
12652
12653 let mut result = None;
12657 for _ in 0..100 {
12658 tokio::time::sleep(Duration::from_millis(20)).await;
12659 let (tx, rx) = oneshot::channel();
12660 handle
12661 .cmd_tx
12662 .send(TorrentCommand::GetMeta { reply: tx })
12663 .await
12664 .expect("cmd_tx send");
12665 let r = rx.await.expect("GetMeta reply");
12666 if r.is_some() {
12667 result = r;
12668 break;
12669 }
12670 }
12671 let meta = result.expect("GetMeta must return Some after PreResolvedMetadata");
12672 assert_eq!(meta.info_hash, info_hash);
12673
12674 handle.shutdown().await.unwrap();
12675 }
12676
12677 #[tokio::test]
12680 async fn web_seed_progress_idle_to_active_on_first_success() {
12681 let mut actor = TorrentActor::for_throttle_test(8, 0);
12682 actor.handle_web_seed_progress("http://seed.example/file", 1024, 1_000_000, None);
12683 let stats = actor
12684 .web_seed_stats
12685 .get("http://seed.example/file")
12686 .expect("stats inserted");
12687 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12688 assert_eq!(stats.downloaded_bytes, 1024);
12689 assert_eq!(stats.last_rate_bps, 1_000_000);
12690 assert_eq!(stats.consecutive_failures, 0);
12691 assert!(stats.last_attempt_unix_secs > 0);
12692 assert!(actor.need_save_resume);
12693 }
12694
12695 #[tokio::test]
12696 async fn web_seed_progress_active_to_errored_then_recovery_persists_last_error() {
12697 let mut actor = TorrentActor::for_throttle_test(8, 0);
12698 let url = "http://seed.example/file".to_string();
12699
12700 actor.handle_web_seed_progress(&url, 1024, 100, None);
12702 assert_eq!(
12703 actor.web_seed_stats[&url].state,
12704 irontide_core::WebSeedState::Active
12705 );
12706
12707 actor.handle_web_seed_progress(&url, 1024, 0, Some("503".into()));
12709 let stats = &actor.web_seed_stats[&url];
12710 assert_eq!(stats.state, irontide_core::WebSeedState::Errored);
12711 assert_eq!(stats.last_error.as_deref(), Some("503"));
12712 assert_eq!(stats.consecutive_failures, 1);
12713
12714 actor.handle_web_seed_progress(&url, 2048, 200, None);
12716 let stats = &actor.web_seed_stats[&url];
12717 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12718 assert_eq!(
12719 stats.last_error.as_deref(),
12720 Some("503"),
12721 "last_error must persist through recovery (D-eng-8)"
12722 );
12723 assert_eq!(
12724 stats.consecutive_failures, 0,
12725 "consecutive_failures resets on success"
12726 );
12727 }
12728
12729 #[tokio::test]
12730 async fn web_seed_progress_consecutive_failures_monotonic_within_run() {
12731 let mut actor = TorrentActor::for_throttle_test(8, 0);
12732 let url = "http://seed.example/file".to_string();
12733
12734 actor.handle_web_seed_progress(&url, 0, 0, Some("e1".into()));
12735 actor.handle_web_seed_progress(&url, 0, 0, Some("e2".into()));
12736 actor.handle_web_seed_progress(&url, 0, 0, Some("e3".into()));
12737 let stats = &actor.web_seed_stats[&url];
12738 assert_eq!(stats.consecutive_failures, 3);
12739 assert_eq!(
12740 stats.last_error.as_deref(),
12741 Some("e3"),
12742 "last_error reflects most recent message"
12743 );
12744
12745 actor.handle_web_seed_progress(&url, 1024, 100, None);
12746 assert_eq!(
12747 actor.web_seed_stats[&url].consecutive_failures, 0,
12748 "success resets consecutive_failures"
12749 );
12750 }
12751
12752 fn install_peer_states(actor: &mut TorrentActor) {
12757 let (queue_tx, _queue_rx) = mpsc::unbounded_channel();
12758 actor.peer_states = Some(std::sync::Arc::new(crate::peer_states::PeerStates::new(
12759 queue_tx,
12760 )));
12761 }
12762
12763 fn addr(octet: u8, port: u16) -> std::net::SocketAddr {
12764 std::net::SocketAddr::new(
12765 std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 0, 2, octet)),
12766 port,
12767 )
12768 }
12769
12770 #[tokio::test]
12771 async fn pex_count_dedups_same_peer_in_two_messages() {
12772 let mut actor = TorrentActor::for_throttle_test(8, 0);
12773 install_peer_states(&mut actor);
12774
12775 actor.handle_add_peers(
12777 vec![addr(1, 6881), addr(2, 6881)],
12778 crate::peer_state::PeerSource::Pex,
12779 );
12780 actor.handle_add_peers(
12782 vec![addr(1, 6881), addr(3, 6881)],
12783 crate::peer_state::PeerSource::Pex,
12784 );
12785 assert_eq!(
12786 actor.pex_peer_count, 3,
12787 "3 unique peers across 2 PEX messages, A counted once"
12788 );
12789 assert_eq!(actor.lsd_peer_count, 0, "LSD untouched");
12790 }
12791
12792 #[tokio::test]
12793 async fn lsd_count_aggregates_across_multicasts() {
12794 let mut actor = TorrentActor::for_throttle_test(8, 0);
12795 install_peer_states(&mut actor);
12796
12797 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd);
12798 actor.handle_add_peers(
12799 vec![addr(2, 6881), addr(3, 6881)],
12800 crate::peer_state::PeerSource::Lsd,
12801 );
12802 actor.handle_add_peers(
12803 vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd,
12805 );
12806 assert_eq!(actor.lsd_peer_count, 3);
12807 }
12808
12809 #[tokio::test]
12810 async fn other_sources_do_not_bump_pex_or_lsd() {
12811 let mut actor = TorrentActor::for_throttle_test(8, 0);
12812 install_peer_states(&mut actor);
12813
12814 actor.handle_add_peers(
12815 vec![addr(1, 6881), addr(2, 6881)],
12816 crate::peer_state::PeerSource::Tracker,
12817 );
12818 actor.handle_add_peers(vec![addr(3, 6881)], crate::peer_state::PeerSource::Dht);
12819 actor.handle_add_peers(vec![addr(4, 6881)], crate::peer_state::PeerSource::Incoming);
12820 assert_eq!(actor.pex_peer_count, 0);
12821 assert_eq!(actor.lsd_peer_count, 0);
12822 }
12823
12824 #[tokio::test]
12825 async fn dedup_runs_against_global_seen_set() {
12826 let mut actor = TorrentActor::for_throttle_test(8, 0);
12832 install_peer_states(&mut actor);
12833
12834 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Tracker);
12835 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Pex);
12836 assert_eq!(
12837 actor.pex_peer_count, 0,
12838 "peer already seen via tracker — PEX shouldn't re-count"
12839 );
12840 }
12841
12842 #[tokio::test]
12843 async fn web_seed_progress_dirties_resume_flag() {
12844 let mut actor = TorrentActor::for_throttle_test(8, 0);
12845 actor.need_save_resume = false;
12846 actor.handle_web_seed_progress("http://x/file", 100, 50, None);
12847 assert!(
12848 actor.need_save_resume,
12849 "every progress event should mark fast-resume dirty"
12850 );
12851 }
12852
12853 #[tokio::test]
12854 async fn paused_torrent_rejects_outbound_peer_connect() {
12855 let mut actor = TorrentActor::for_throttle_test(8, 0);
12856 install_peer_states(&mut actor);
12857 actor.state = TorrentState::Paused;
12858
12859 let sem = Arc::new(tokio::sync::Semaphore::new(1));
12860 let permit = sem.clone().acquire_owned().await.unwrap();
12861 let connect = crate::peer_adder::ConnectPeer {
12862 addr: addr(1, 6881),
12863 source: crate::peer_state::PeerSource::Dht,
12864 permit,
12865 };
12866 actor.handle_adder_connect(connect);
12867 assert!(
12868 actor.peers.is_empty(),
12869 "paused torrent must not accept outbound peer connections"
12870 );
12871 assert_eq!(
12872 sem.available_permits(),
12873 1,
12874 "semaphore permit must be released on rejection"
12875 );
12876 }
12877
12878 #[tokio::test]
12879 async fn resume_from_queued_restores_fetching_metadata_for_magnets() {
12880 let mut actor = TorrentActor::for_throttle_test(0, 0);
12881 actor.state = TorrentState::Queued;
12882 assert!(
12883 actor.chunk_tracker.is_none(),
12884 "magnet torrent has no chunk tracker before metadata"
12885 );
12886 assert_eq!(actor.num_pieces, 0);
12887
12888 actor.handle_resume().await;
12889 assert_eq!(
12890 actor.state,
12891 TorrentState::FetchingMetadata,
12892 "magnet torrent must resume to FetchingMetadata, not Downloading"
12893 );
12894 }
12895
12896 #[tokio::test]
12897 async fn resume_from_queued_restores_downloading_when_metadata_known() {
12898 let mut actor = TorrentActor::for_throttle_test(8, 0);
12899 actor.state = TorrentState::Queued;
12900
12901 actor.handle_resume().await;
12902 assert_eq!(
12903 actor.state,
12904 TorrentState::Downloading,
12905 "torrent with known pieces must resume to Downloading"
12906 );
12907 }
12908
12909 #[tokio::test]
12910 async fn queued_torrent_rejects_outbound_peer_connect() {
12911 let mut actor = TorrentActor::for_throttle_test(8, 0);
12912 install_peer_states(&mut actor);
12913 actor.state = TorrentState::Queued;
12914
12915 let sem = Arc::new(tokio::sync::Semaphore::new(1));
12916 let permit = sem.clone().acquire_owned().await.unwrap();
12917 let connect = crate::peer_adder::ConnectPeer {
12918 addr: addr(1, 6881),
12919 source: crate::peer_state::PeerSource::Dht,
12920 permit,
12921 };
12922 actor.handle_adder_connect(connect);
12923 assert!(
12924 actor.peers.is_empty(),
12925 "queued torrent must not accept outbound peer connections"
12926 );
12927 assert_eq!(
12928 sem.available_permits(),
12929 1,
12930 "semaphore permit must be released on rejection"
12931 );
12932 }
12933
12934 fn inject_peer_for_flush(
12938 actor: &mut TorrentActor,
12939 peer_addr: std::net::SocketAddr,
12940 unchoke_started: Option<std::time::Instant>,
12941 prior_total: std::time::Duration,
12942 ) {
12943 let (cmd_tx, _cmd_rx) = mpsc::channel(8);
12944 let mut peer = crate::peer_state::PeerState::new(
12945 peer_addr,
12946 actor.num_pieces,
12947 cmd_tx,
12948 crate::peer_state::PeerSource::Tracker,
12949 Arc::new(AtomicU32::new(0)),
12950 Arc::new(AtomicU32::new(128)),
12951 Arc::new(tokio::sync::Notify::new()),
12952 );
12953 peer.am_unchoke_started_at = unchoke_started;
12954 peer.unchoke_duration_total = prior_total;
12955 actor.peers.insert(peer_addr, peer);
12956 }
12957
12958 #[tokio::test]
12959 async fn disconnect_while_unchoked_flushes_delta_into_torrent_map() {
12960 let mut actor = TorrentActor::for_throttle_test(8, 0);
12961 let p = addr(1, 6881);
12962
12963 inject_peer_for_flush(
12966 &mut actor,
12967 p,
12968 Some(std::time::Instant::now() - std::time::Duration::from_millis(50)),
12969 std::time::Duration::from_millis(100),
12970 );
12971
12972 actor.disconnect_peer(p, "test");
12973
12974 let total = actor
12975 .unchoke_durations
12976 .get(&p)
12977 .copied()
12978 .expect("disconnect must flush a non-zero delta into the torrent map");
12979 assert!(
12980 total >= std::time::Duration::from_millis(140),
12981 "expected ≥140 ms (100 prior + ~50 in-flight), got {total:?}"
12982 );
12983 }
12984
12985 #[tokio::test]
12986 async fn disconnect_then_reconnect_preserves_history() {
12987 let mut actor = TorrentActor::for_throttle_test(8, 0);
12988 let p = addr(2, 6881);
12989
12990 inject_peer_for_flush(&mut actor, p, None, std::time::Duration::from_millis(80));
12992 actor.disconnect_peer(p, "test");
12993 let after_first = *actor
12994 .unchoke_durations
12995 .get(&p)
12996 .expect("first flush must populate the entry");
12997 assert_eq!(after_first, std::time::Duration::from_millis(80));
12998
12999 inject_peer_for_flush(
13001 &mut actor,
13002 p,
13003 Some(std::time::Instant::now() - std::time::Duration::from_millis(40)),
13004 std::time::Duration::ZERO,
13005 );
13006 actor.disconnect_peer(p, "test");
13007 let after_second = *actor.unchoke_durations.get(&p).unwrap();
13008 assert!(
13009 after_second >= std::time::Duration::from_millis(120),
13010 "second flush must add to the existing entry, got {after_second:?}"
13011 );
13012 }
13013
13014 #[tokio::test]
13017 async fn piece_verified_wakes_reservation_notify() {
13018 let mut actor = TorrentActor::for_throttle_test(8, 0);
13019 let notify = Arc::new(tokio::sync::Notify::new());
13020 actor.reservation_notify = Some(Arc::clone(¬ify));
13021
13022 let notified = notify.notified();
13023 tokio::pin!(notified);
13024 assert!(
13025 futures::poll!(&mut notified).is_pending(),
13026 "notify should not have fired yet"
13027 );
13028
13029 actor.on_piece_verified(0).await;
13030
13031 tokio::time::timeout(Duration::from_secs(1), notified)
13032 .await
13033 .expect("reservation_notify must be woken by on_piece_verified");
13034 }
13035
13036 fn actor_with_tracker_state(queue: u32, inflight: u32) -> TorrentActor {
13042 use crate::piece_reservation::PieceTracker;
13043 use irontide_storage::Bitfield;
13044 let mut actor = TorrentActor::for_throttle_test(8, 0);
13045 let num_pieces = queue + inflight + 1;
13046 let we_have = Bitfield::new(num_pieces);
13047 let mut wanted = Bitfield::new(num_pieces);
13048 for i in 0..num_pieces {
13049 wanted.set(i);
13050 }
13051 let mut pt = PieceTracker::new(num_pieces, &we_have, &wanted);
13052 for i in queue..num_pieces {
13055 pt.mark_unwanted(i);
13056 }
13057 for i in 0..inflight {
13059 pt.record_reservation(i, "10.0.0.1:6881".parse().unwrap());
13060 }
13061 actor.piece_tracker = Some(pt);
13066 actor
13067 }
13068
13069 #[tokio::test]
13070 async fn pipeline_tick_skips_wake_when_dispatch_state_unchanged() {
13071 let mut actor = actor_with_tracker_state(10, 3);
13072 let notify = Arc::new(tokio::sync::Notify::new());
13073 actor.reservation_notify = Some(Arc::clone(¬ify));
13074
13075 actor.tick_dispatch_safety_wake();
13079 let _drain = notify.notified();
13080
13081 let notified = notify.notified();
13083 tokio::pin!(notified);
13084 actor.tick_dispatch_safety_wake();
13085
13086 tokio::task::yield_now().await;
13088 assert!(
13089 futures::poll!(&mut notified).is_pending(),
13090 "tick must not wake when (queue_count, inflight_count) is unchanged"
13091 );
13092 let skipped = actor.counters.get(crate::stats::DISPATCH_TICK_WAKE_SKIPPED);
13094 assert!(
13095 skipped >= 1,
13096 "expected DISPATCH_TICK_WAKE_SKIPPED >= 1, got {skipped}"
13097 );
13098 }
13099
13100 #[tokio::test]
13101 async fn pipeline_tick_wakes_when_inflight_changes() {
13102 let mut actor = actor_with_tracker_state(10, 3);
13103 let notify = Arc::new(tokio::sync::Notify::new());
13104 actor.reservation_notify = Some(Arc::clone(¬ify));
13105
13106 actor.tick_dispatch_safety_wake();
13108
13109 if let Some(ref mut pt) = actor.piece_tracker {
13112 pt.record_reservation(5, "10.0.0.2:6881".parse().unwrap());
13113 }
13114
13115 let notified = notify.notified();
13116 tokio::pin!(notified);
13117 actor.tick_dispatch_safety_wake();
13118
13119 tokio::time::timeout(Duration::from_secs(1), notified)
13120 .await
13121 .expect("tick must wake when dispatch state changed");
13122 }
13123}