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")]
2055 pub async fn test_inject_metadata(&self, info_bytes: Vec<u8>) -> crate::Result<()> {
2056 let (tx, rx) = tokio::sync::oneshot::channel();
2057 self.cmd_tx
2058 .send(TorrentCommand::TestInjectMetadata {
2059 info_bytes,
2060 reply: tx,
2061 })
2062 .await
2063 .map_err(|_| crate::Error::Shutdown)?;
2064 rx.await.map_err(|_| crate::Error::Shutdown)?;
2065 Ok(())
2066 }
2067}
2068
2069#[derive(Debug, Clone)]
2075pub(crate) struct CachedFileEntry {
2076 pub(crate) index: usize,
2077 #[allow(dead_code)] pub(crate) length: u64,
2079 pub(crate) first_piece: u32,
2080 pub(crate) last_piece: u32,
2081}
2082
2083#[derive(Debug, Clone)]
2085pub(crate) struct CachedFileInfo {
2086 pub(crate) entries: Vec<CachedFileEntry>,
2087}
2088
2089pub(crate) fn build_cached_file_info(meta: &TorrentMetaV1, lengths: &Lengths) -> CachedFileInfo {
2090 let piece_length = lengths.piece_length();
2091 let files = meta.info.files();
2092 let mut entries = Vec::with_capacity(files.len());
2093 let mut offset = 0u64;
2094 for (index, file) in files.iter().enumerate() {
2095 let first_piece = (offset / piece_length) as u32;
2096 let last_piece = if file.length == 0 {
2097 first_piece
2098 } else {
2099 ((offset + file.length - 1) / piece_length) as u32
2100 };
2101 entries.push(CachedFileEntry {
2102 index,
2103 length: file.length,
2104 first_piece,
2105 last_piece,
2106 });
2107 offset += file.length;
2108 }
2109 CachedFileInfo { entries }
2110}
2111
2112pub(crate) struct TorrentActor {
2117 pub(crate) config: TorrentConfig,
2118 pub(crate) lock_timing: crate::timed_lock::LockTimingSettings,
2120 pub(crate) info_hash: Id20,
2121 pub(crate) our_peer_id: Id20,
2122 pub(crate) state: TorrentState,
2123
2124 pub(crate) disk: Option<DiskHandle>,
2126 pub(crate) disk_manager: DiskManagerHandle,
2127 pub(crate) chunk_tracker: Option<ChunkTracker>,
2128 pub(crate) lengths: Option<Lengths>,
2129 pub(crate) num_pieces: u32,
2130
2131 pub(crate) file_priorities: Vec<FilePriority>,
2133 pub(crate) wanted_pieces: Bitfield,
2134 pub(crate) end_game: EndGame,
2135
2136 pub(crate) streaming_pieces: BTreeSet<u32>,
2138 pub(crate) time_critical_pieces: BTreeSet<u32>,
2139 pub(crate) streaming_cursors: Vec<crate::streaming::StreamingCursor>,
2140 pub(crate) piece_ready_tx: broadcast::Sender<u32>,
2141 pub(crate) have_watch_tx: tokio::sync::watch::Sender<Bitfield>,
2142 pub(crate) have_watch_rx: tokio::sync::watch::Receiver<Bitfield>,
2143 pub(crate) stream_read_semaphore: Arc<tokio::sync::Semaphore>,
2144
2145 pub(crate) peers: HashMap<SocketAddr, PeerState>,
2147 pub(crate) unchoke_durations: HashMap<SocketAddr, Duration>,
2156 pub(crate) cached_peer_rates: FxHashMap<SocketAddr, f64>,
2159 #[allow(dead_code)]
2161 pub(crate) refill_notify: Arc<tokio::sync::Notify>,
2162 pub(crate) atomic_states: Option<Arc<crate::piece_reservation::AtomicPieceStates>>,
2164 pub(crate) block_maps: Option<Arc<BlockMaps>>,
2166 pub(crate) steal_candidates: Option<Arc<StealCandidates>>,
2168 pub(crate) last_steal_populate: Instant,
2170 pub(crate) piece_write_guards: Option<Arc<crate::piece_reservation::PieceWriteGuards>>,
2172 pub(crate) soft_reap_buf: Vec<std::net::SocketAddr>,
2176 pub(crate) eviction_history: std::collections::VecDeque<std::time::Instant>,
2181 pub(crate) force_immediate_choker_tick: bool,
2186 pub(crate) piece_tracker: Option<PieceTracker>,
2188 pub(crate) order_map_tx: tokio::sync::watch::Sender<Arc<PieceOrderMap>>,
2190
2191 pub(crate) order_map_dirty: bool,
2196
2197 pub(crate) next_order_map_gen: u64,
2203 pub(crate) piece_owner: Vec<Option<u16>>,
2205 pub(crate) peer_slab: crate::piece_reservation::PeerSlab,
2207 #[allow(dead_code)]
2208 pub(crate) priority_pieces: BTreeSet<u32>,
2209 pub(crate) max_in_flight: usize,
2211 pub(crate) reservation_notify: Option<Arc<tokio::sync::Notify>>,
2213 pub(crate) last_tick_dispatch_state: Option<(u32, usize)>,
2221 pub(crate) choker: Choker,
2222 pub(crate) user_seed_mode: bool,
2229 pub(crate) user_forced: bool,
2231 pub(crate) max_connections: usize,
2233 pub(crate) peer_states: Option<Arc<crate::peer_states::PeerStates>>,
2235 pub(crate) connect_semaphore: Arc<tokio::sync::Semaphore>,
2238 pub(crate) connect_permits:
2241 HashMap<SocketAddr, Arc<parking_lot::Mutex<Option<tokio::sync::OwnedSemaphorePermit>>>>,
2242 pub(crate) connect_rx: Option<mpsc::Receiver<ConnectPeer>>,
2244
2245 pub(crate) metadata_downloader: Option<MetadataDownloader>,
2247
2248 pub(crate) meta: Option<TorrentMetaV1>,
2250
2251 pub(crate) cached_files: Option<CachedFileInfo>,
2253
2254 pub(crate) downloaded: u64,
2256 pub(crate) uploaded: u64,
2257 pub(crate) checking_progress: f32,
2258 pub(crate) total_download: u64,
2259 pub(crate) total_upload: u64,
2260 pub(crate) total_failed_bytes: u64,
2261 pub(crate) total_redundant_bytes: u64,
2262 pub(crate) added_time: i64,
2263 pub(crate) completed_time: i64,
2264 pub(crate) last_download: i64,
2265 pub(crate) last_upload: i64,
2266 pub(crate) last_seen_complete: i64,
2267 pub(crate) active_duration: i64,
2268 pub(crate) finished_duration: i64,
2269 pub(crate) seeding_duration: i64,
2270 pub(crate) active_since: Option<std::time::Instant>,
2271 pub(crate) state_duration_since: Option<std::time::Instant>,
2272 #[allow(dead_code)] pub(crate) started_at: std::time::Instant,
2274 pub(crate) moving_storage: bool,
2275 pub(crate) has_incoming: bool,
2276 pub(crate) need_save_resume: bool,
2277 pub(crate) error: String,
2278 pub(crate) error_file: i32,
2279
2280 pub(crate) cmd_rx: mpsc::Receiver<TorrentCommand>,
2282 pub(crate) event_tx: mpsc::Sender<PeerEvent>,
2283 pub(crate) event_rx: mpsc::Receiver<PeerEvent>,
2284
2285 pub(crate) write_error_rx: mpsc::Receiver<crate::disk::DiskWriteError>,
2287 pub(crate) write_error_tx: mpsc::Sender<crate::disk::DiskWriteError>,
2288 pub(crate) verify_result_rx: mpsc::Receiver<crate::disk::VerifyResult>,
2289 pub(crate) verify_result_tx: mpsc::Sender<crate::disk::VerifyResult>,
2290 pub(crate) pending_verify: HashSet<u32>,
2293 pub(crate) piece_generations: Vec<u64>,
2296 pub(crate) hash_result_rx: tokio::sync::mpsc::Receiver<crate::hash_pool::HashResult>,
2298 pub(crate) hash_result_tx: tokio::sync::mpsc::Sender<crate::hash_pool::HashResult>,
2300
2301 pub(crate) listener: Option<Box<dyn crate::transport::TransportListener>>,
2303
2304 pub(crate) utp_socket: Option<irontide_utp::UtpSocket>,
2306 pub(crate) utp_socket_v6: Option<irontide_utp::UtpSocket>,
2308
2309 pub(crate) tracker_manager: TrackerManager,
2311 pub(crate) tracker_result_rx: Option<mpsc::Receiver<crate::tracker_manager::TrackerPeerBatch>>,
2314
2315 pub(crate) dht_rx: irontide_dht::DhtReceiver,
2322 pub(crate) dht_v6_rx: irontide_dht::DhtReceiver,
2323 pub(crate) dht_enabled: bool,
2324 pub(crate) dht_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2325 pub(crate) dht_v6_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2326 pub(crate) dht_v6_empty_count: u32,
2329 pub(crate) dht_v6_last_retry: Option<std::time::Instant>,
2331
2332 pub(crate) alert_tx: broadcast::Sender<Alert>,
2334 pub(crate) alert_mask: Arc<AtomicU32>,
2335
2336 pub(crate) upload_bucket: crate::rate_limiter::TokenBucket,
2338 pub(crate) download_bucket: SharedBucket,
2339 pub(crate) global_upload_bucket: Option<SharedBucket>,
2340 #[allow(dead_code)] pub(crate) global_download_bucket: Option<SharedBucket>,
2342 pub(crate) slot_tuner: crate::slot_tuner::SlotTuner,
2343 pub(crate) upload_bytes_interval: u64,
2344
2345 pub(crate) peak_download_rate: u64,
2347
2348 pub(crate) web_seeds: HashMap<String, mpsc::Sender<crate::web_seed::WebSeedCommand>>,
2350 pub(crate) banned_web_seeds: HashSet<String>,
2351 pub(crate) web_seed_in_flight: HashMap<u32, String>,
2352 pub(crate) web_seed_stats: HashMap<String, irontide_core::WebSeedStats>,
2356 pub(crate) pex_peer_count: usize,
2362 pub(crate) lsd_peer_count: usize,
2366
2367 pub(crate) super_seed: Option<crate::super_seed::SuperSeedState>,
2369 pub(crate) have_broadcast_tx: tokio::sync::broadcast::Sender<u32>,
2371
2372 pub(crate) suggested_to_peers: HashMap<SocketAddr, HashSet<u32>>,
2374
2375 pub(crate) predictive_have_sent: HashSet<u32>,
2377
2378 pub(crate) ban_manager: irontide_session_types::SharedBanManager,
2380 pub(crate) piece_contributors: HashMap<u32, HashSet<std::net::IpAddr>>,
2381 pub(crate) parole_pieces: HashMap<u32, crate::ban::ParoleState>,
2382
2383 pub(crate) ip_filter: irontide_session_types::SharedIpFilter,
2385
2386 pub(crate) external_ip: Option<std::net::IpAddr>,
2388
2389 pub(crate) share_lru: std::collections::VecDeque<u32>,
2393 pub(crate) share_max_pieces: usize,
2395
2396 pub(crate) plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
2398
2399 pub(crate) hash_picker: Option<irontide_core::HashPicker>,
2401 pub(crate) version: irontide_core::TorrentVersion,
2402 #[allow(dead_code)] pub(crate) meta_v2: Option<irontide_core::TorrentMetaV2>,
2404
2405 pub(crate) info_hashes: irontide_core::InfoHashes,
2407
2408 pub(crate) dht_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2410 pub(crate) dht_v6_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2411
2412 pub(crate) magnet_selected_files: Option<Vec<irontide_core::FileSelection>>,
2415
2416 pub(crate) sam_session: Option<Arc<crate::i2p::SamSession>>,
2418
2419 pub(crate) i2p_accept_rx: Option<mpsc::Receiver<crate::i2p::SamStream>>,
2421
2422 pub(crate) i2p_peer_counter: u32,
2424
2425 pub(crate) i2p_destinations: HashMap<SocketAddr, crate::i2p::I2pDestination>,
2427
2428 pub(crate) ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
2430
2431 pub(crate) rate_limiter_set: crate::rate_limiter::RateLimiterSet,
2433 pub(crate) auto_sequential_active: bool,
2435 pub(crate) factory: Arc<crate::transport::NetworkFactory>,
2437 pub(crate) hash_pool_ref: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
2439 pub(crate) live_outgoing_peers:
2441 std::sync::Arc<parking_lot::RwLock<std::collections::HashMap<SocketAddr, u8>>>,
2442 pub(crate) connect_attempts: u64,
2444 pub(crate) connect_failures: u64,
2446 pub(crate) choke_rotations: u64,
2448 pub(crate) inflight_started: Vec<Option<Instant>>,
2451 pub(crate) completed_piece_times: std::collections::VecDeque<Duration>,
2453 pub(crate) piece_steals: u64,
2455 pub(crate) holepunch_relayed: u64,
2457 pub(crate) holepunch_relay_rate: HashMap<SocketAddr, (Instant, u32)>,
2459 pub(crate) holepunch_cooldowns: HashMap<SocketAddr, Instant>,
2461 pub(crate) holepunch_pending: Vec<SocketAddr>,
2463 pub(crate) counters: Arc<crate::stats::SessionCounters>,
2467}
2468
2469pub(crate) const END_GAME_DEPTH: usize = 128;
2476
2477impl TorrentActor {
2480 pub(crate) fn current_dht(&self) -> Option<irontide_dht::DhtHandle> {
2486 if self.dht_enabled {
2487 self.dht_rx.current()
2488 } else {
2489 None
2490 }
2491 }
2492
2493 pub(crate) fn current_dht_v6(&self) -> Option<irontide_dht::DhtHandle> {
2496 if self.dht_enabled {
2497 self.dht_v6_rx.current()
2498 } else {
2499 None
2500 }
2501 }
2502
2503 #[allow(dead_code)] pub(crate) async fn current_dht_or_wait(
2519 &mut self,
2520 hold: std::time::Duration,
2521 ) -> Option<irontide_dht::DhtHandle> {
2522 if !self.dht_enabled {
2523 return None;
2524 }
2525 if let Some(handle) = self.dht_rx.current() {
2526 return Some(handle);
2527 }
2528 match tokio::time::timeout(hold, self.dht_rx.changed()).await {
2530 Ok(Ok(())) => self.dht_rx.current(),
2531 Ok(Err(_)) | Err(_) => None,
2532 }
2533 }
2534
2535 async fn run(mut self) {
2537 self.verify_existing_pieces().await;
2539
2540 if let Some(ct) = &self.chunk_tracker {
2543 let atomic_states = Arc::new(AtomicPieceStates::new(
2544 self.num_pieces,
2545 ct.bitfield(),
2546 &self.wanted_pieces,
2547 ));
2548 self.atomic_states = Some(Arc::clone(&atomic_states));
2549 self.piece_owner = vec![None; self.num_pieces as usize];
2550 self.inflight_started = vec![None; self.num_pieces as usize];
2552 self.max_in_flight = self.config.max_in_flight_pieces;
2553
2554 if self.config.use_block_stealing {
2556 if let Some(ref lengths) = self.lengths {
2557 self.block_maps = Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
2558 }
2559 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
2560 }
2561 self.piece_write_guards = Some(Arc::new(
2563 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
2564 ));
2565
2566 self.piece_tracker = Some(PieceTracker::new(
2568 self.num_pieces,
2569 ct.bitfield(),
2570 &self.wanted_pieces,
2571 ));
2572 if let Some(ref cached) = self.cached_files {
2573 let file_piece_ranges: Vec<(u32, u32)> = cached
2574 .entries
2575 .iter()
2576 .map(|e| (e.first_piece, e.last_piece))
2577 .collect();
2578 let om = Arc::new(PieceOrderMap::build(
2579 &self.file_priorities,
2580 &file_piece_ranges,
2581 self.num_pieces,
2582 0,
2583 ));
2584 self.order_map_tx.send_replace(om);
2585 }
2586
2587 let notify = Arc::new(tokio::sync::Notify::new());
2588 self.reservation_notify = Some(notify);
2589 }
2590
2591 if self.state != TorrentState::Seeding {
2593 self.spawn_web_seeds();
2594 self.assign_pieces_to_web_seeds();
2595 }
2596
2597 let connect_semaphore = Arc::new(tokio::sync::Semaphore::new(
2600 self.effective_max_connections(),
2601 ));
2602 self.connect_semaphore = Arc::clone(&connect_semaphore);
2603 self.connect_permits.clear();
2604 let (queue_tx, queue_rx) = mpsc::unbounded_channel();
2608 let peer_states = Arc::new(crate::peer_states::PeerStates::new_with_config(
2609 queue_tx,
2610 self.config.eviction_ban_set_cap,
2611 std::time::Duration::from_secs(self.config.eviction_ban_duration_secs),
2612 ));
2613 self.peer_states = Some(Arc::clone(&peer_states));
2614 let (adder_connect_tx, adder_connect_rx) = mpsc::channel(64);
2615 self.connect_rx = Some(adder_connect_rx);
2616 tokio::spawn(peer_adder::peer_adder_task(
2618 queue_rx,
2619 Arc::clone(&connect_semaphore),
2620 Arc::clone(&peer_states),
2621 Arc::clone(&self.ban_manager),
2622 Arc::clone(&self.ip_filter),
2623 adder_connect_tx,
2624 ));
2625
2626 let mut unchoke_interval = tokio::time::interval(Duration::from_secs(10));
2627 let mut rate_interval = tokio::time::interval(Duration::from_secs(2));
2628 rate_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2629 let mut optimistic_interval = tokio::time::interval(Duration::from_secs(30));
2630 let mut refill_interval = tokio::time::interval(Duration::from_millis(100));
2631 let mut dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::ZERO));
2632 let mut suggest_interval = if self.config.suggest_mode {
2633 Some(tokio::time::interval(Duration::from_secs(30)))
2634 } else {
2635 None
2636 };
2637 let mut turnover_interval = tokio::time::interval(Duration::from_secs(1));
2639 let mut pipeline_tick_interval = tokio::time::interval(Duration::from_secs(1));
2640 pipeline_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2642 let mut end_game_tick_interval = tokio::time::interval(Duration::from_millis(200));
2643 end_game_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2644 let mut diag_interval = tokio::time::interval(Duration::from_secs(5));
2645 let mut conn_stats_interval = tokio::time::interval(Duration::from_secs(30));
2647 let mut metadata_timeout_interval = tokio::time::interval(Duration::from_secs(5));
2649 let mut soft_reap_interval = tokio::time::interval(Duration::from_secs(1));
2652 let mut eviction_interval = tokio::time::interval(Duration::from_secs(2));
2655 eviction_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2656
2657 unchoke_interval.tick().await;
2659 optimistic_interval.tick().await;
2660 refill_interval.tick().await;
2661 if let Some(ref mut si) = suggest_interval {
2663 si.tick().await; }
2665 turnover_interval.tick().await;
2666 pipeline_tick_interval.tick().await;
2667 end_game_tick_interval.tick().await;
2668 diag_interval.tick().await;
2669 conn_stats_interval.tick().await;
2670 metadata_timeout_interval.tick().await;
2671 soft_reap_interval.tick().await;
2672 eviction_interval.tick().await;
2673
2674 if self.state == TorrentState::Downloading && self.config.enable_dht {
2677 if let Some(dht) = self.current_dht()
2679 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
2680 {
2681 warn!("DHT v4 announce failed: {e}");
2682 }
2683 if let Some(dht6) = self.current_dht_v6()
2684 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
2685 {
2686 debug!("DHT v6 announce failed: {e}");
2687 }
2688 if self.info_hashes.is_hybrid()
2690 && let Some(v2) = self.info_hashes.v2
2691 {
2692 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
2693 if v2_as_v1 != self.info_hash {
2694 if let Some(dht) = self.current_dht()
2695 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
2696 {
2697 debug!("DHT v4 dual-swarm announce failed: {e}");
2698 }
2699 if let Some(dht6) = self.current_dht_v6()
2700 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
2701 {
2702 debug!("DHT v6 dual-swarm announce failed: {e}");
2703 }
2704 }
2705 }
2706 }
2707
2708 if self.config.enable_i2p
2711 && let Some(ref sam) = self.sam_session
2712 {
2713 let (tx, rx) = mpsc::channel(16);
2714 let sam = Arc::clone(sam);
2715 tokio::spawn(async move {
2716 loop {
2717 match sam.accept().await {
2718 Ok(stream) => {
2719 if tx.send(stream).await.is_err() {
2720 break; }
2722 }
2723 Err(e) => {
2724 warn!("I2P accept error: {e}");
2725 tokio::time::sleep(Duration::from_secs(5)).await;
2726 }
2727 }
2728 }
2729 });
2730 self.i2p_accept_rx = Some(rx);
2731 }
2732
2733 loop {
2734 tokio::select! {
2735 biased;
2736 event = self.event_rx.recv() => {
2741 if let Some(event) = event {
2742 Self::ping_event_drain(&self.peers, &event);
2750 self.handle_peer_event(event)
2751 .instrument(tracing::debug_span!("handle_peer_event"))
2752 .await;
2753 for _ in 0..512 {
2755 match self.event_rx.try_recv() {
2756 Ok(event) => {
2757 Self::ping_event_drain(&self.peers, &event);
2758 self.handle_peer_event(event).await;
2759 }
2760 Err(_) => break,
2761 }
2762 }
2763 }
2764 }
2765 Some(result) = self.verify_result_rx.recv() => {
2767 self.pending_verify.remove(&result.piece);
2768 let dominated = self.chunk_tracker.as_ref()
2770 .is_some_and(|ct| ct.bitfield().get(result.piece));
2771 if !dominated {
2772 if result.passed {
2773 self.on_piece_verified(result.piece).await;
2774 } else {
2775 self.on_piece_hash_failed(result.piece).await;
2776 }
2778 }
2779 }
2780 Some(result) = self.hash_result_rx.recv() => {
2782 self.handle_hash_result(result).await;
2783 }
2784 cmd = self.cmd_rx.recv() => {
2786 match cmd {
2787 Some(TorrentCommand::AddPeers { peers, source }) => {
2788 self.handle_add_peers(peers, source);
2789 }
2790 Some(TorrentCommand::Stats { reply }) => {
2791 let _ = reply.send(self.make_stats());
2792 }
2793 Some(TorrentCommand::Pause) => {
2794 self.handle_pause().await;
2795 }
2796 Some(TorrentCommand::Queue) => {
2797 self.handle_queue();
2798 }
2799 Some(TorrentCommand::Resume) => {
2800 self.handle_resume().await;
2801 }
2802 Some(TorrentCommand::ForceResume) => {
2803 self.user_forced = true;
2804 self.handle_resume().await;
2805 }
2806 Some(TorrentCommand::SetCategory { category, reply }) => {
2807 self.config.category = category;
2811 self.need_save_resume = true;
2812 let _ = reply.send(());
2813 }
2814 Some(TorrentCommand::SetTags { tags, reply }) => {
2815 self.config.tags = tags;
2822 self.need_save_resume = true;
2823 let _ = reply.send(());
2824 }
2825 Some(TorrentCommand::GetWebSeeds { reply }) => {
2826 let urls = match &self.meta {
2832 Some(meta) => {
2833 let mut v = Vec::with_capacity(
2834 meta.url_list.len() + meta.httpseeds.len(),
2835 );
2836 v.extend(meta.url_list.iter().cloned());
2837 v.extend(meta.httpseeds.iter().cloned());
2838 v
2839 }
2840 None => Vec::new(),
2841 };
2842 let _ = reply.send(urls);
2843 }
2844 Some(TorrentCommand::GetPieceStates { reply }) => {
2845 let states = match self.atomic_states.as_ref() {
2849 Some(atomic) => atomic.snapshot(),
2850 None => Vec::new(),
2851 };
2852 let _ = reply.send(states);
2853 }
2854 Some(TorrentCommand::GetPieceHashes { offset, limit, reply }) => {
2855 let offset = offset as usize;
2872 let limit = limit as usize;
2873 let raw: Vec<Vec<u8>> = match self.version {
2874 irontide_core::TorrentVersion::V1Only
2875 | irontide_core::TorrentVersion::Hybrid => self
2876 .meta
2877 .as_ref()
2878 .map(|meta| {
2879 meta.info
2880 .pieces
2881 .chunks_exact(20)
2882 .skip(offset)
2883 .take(limit)
2884 .map(<[u8]>::to_vec)
2885 .collect::<Vec<Vec<u8>>>()
2886 })
2887 .unwrap_or_default(),
2888 irontide_core::TorrentVersion::V2Only => self
2889 .meta_v2
2890 .as_ref()
2891 .map(|m| {
2892 m.piece_layers
2893 .values()
2894 .flat_map(|v| v.chunks_exact(32))
2895 .skip(offset)
2896 .take(limit)
2897 .map(<[u8]>::to_vec)
2898 .collect::<Vec<Vec<u8>>>()
2899 })
2900 .unwrap_or_default(),
2901 };
2902 let _ = reply.send(raw);
2903 }
2904 Some(TorrentCommand::SaveResumeData { reply }) => {
2905 let result = self.build_resume_data();
2906 let _ = reply.send(result);
2907 }
2908 Some(TorrentCommand::TakeResumeIfDirty { reply }) => {
2909 let result = if self.need_save_resume {
2923 let built = self.build_resume_data();
2924 if built.is_ok() {
2925 self.need_save_resume = false;
2926 }
2927 built.map(Some)
2928 } else {
2929 Ok(None)
2930 };
2931 let _ = reply.send(result);
2932 }
2933 Some(TorrentCommand::SetFilePriority { index, priority, reply }) => {
2934 match self.apply_file_priority_scoped(index, priority) {
2944 Ok((first, last)) => {
2945 self.sync_piece_states_for_range(first, last);
2946 if let Some(ref mut pt) = self.piece_tracker {
2947 for piece in first..=last {
2948 if self.wanted_pieces.get(piece) {
2949 pt.mark_wanted(piece);
2950 } else {
2951 pt.mark_unwanted(piece);
2952 }
2953 }
2954 }
2955 self.order_map_dirty = true;
2956 let _ = reply.send(Ok(()));
2957 }
2958 Err(e) => {
2959 let _ = reply.send(Err(e));
2960 }
2961 }
2962 }
2963 Some(TorrentCommand::FilePriorities { reply }) => {
2964 let _ = reply.send(self.file_priorities.clone());
2965 }
2966 Some(TorrentCommand::ForceReannounce) => {
2967 self.tracker_manager.force_reannounce();
2968 }
2969 Some(TorrentCommand::TrackerList { reply }) => {
2970 let _ = reply.send(self.tracker_manager.tracker_list());
2971 }
2972 Some(TorrentCommand::Scrape { reply }) => {
2973 let result = self.tracker_manager.scrape().await;
2974 if let Some((ref url, ref info)) = result {
2975 post_alert(&self.alert_tx, &self.alert_mask, AlertKind::ScrapeReply {
2976 info_hash: self.info_hash,
2977 url: url.clone(),
2978 complete: info.complete,
2979 incomplete: info.incomplete,
2980 downloaded: info.downloaded,
2981 });
2982 }
2983 let _ = reply.send(result);
2984 }
2985 Some(TorrentCommand::OpenFile { file_index, reply }) => {
2986 let result = self.handle_open_file(file_index);
2987 let _ = reply.send(result);
2988 }
2989 Some(TorrentCommand::IncomingPeer { stream, addr }) => {
2990 self.spawn_peer_from_stream_with_mode(
2991 addr,
2992 stream,
2993 Some(irontide_wire::mse::EncryptionMode::Disabled),
2994 );
2995 }
2996 Some(TorrentCommand::UpdateExternalIp { ip }) => {
2997 self.external_ip = Some(ip);
2998 post_alert(
2999 &self.alert_tx,
3000 &self.alert_mask,
3001 AlertKind::ExternalIpDetected { ip },
3002 );
3003 }
3004 Some(TorrentCommand::MoveStorage { new_path, reply }) => {
3005 let result = self.handle_move_storage(new_path).await;
3006 let _ = reply.send(result);
3007 }
3008 Some(TorrentCommand::SpawnSslPeer { addr, stream }) => {
3009 self.spawn_peer_from_stream_with_mode(
3011 addr,
3012 stream.0,
3013 Some(irontide_wire::mse::EncryptionMode::Disabled),
3014 );
3015 }
3016 Some(TorrentCommand::SetDownloadLimit { bytes_per_sec, reply }) => {
3017 self.download_bucket.lock().set_rate(bytes_per_sec);
3018 let _ = reply.send(());
3019 }
3020 Some(TorrentCommand::SetUploadLimit { bytes_per_sec, reply }) => {
3021 self.upload_bucket.set_rate(bytes_per_sec);
3022 let _ = reply.send(());
3023 }
3024 Some(TorrentCommand::DownloadLimit { reply }) => {
3025 let _ = reply.send(self.download_bucket.lock().rate());
3026 }
3027 Some(TorrentCommand::UploadLimit { reply }) => {
3028 let _ = reply.send(self.upload_bucket.rate());
3029 }
3030 Some(TorrentCommand::SetSequentialDownload { enabled, reply }) => {
3031 self.config.sequential_download = enabled;
3032 let _ = reply.send(());
3033 }
3034 Some(TorrentCommand::IsSequentialDownload { reply }) => {
3035 let _ = reply.send(self.config.sequential_download);
3036 }
3037 Some(TorrentCommand::SetSuperSeeding { enabled, reply }) => {
3038 self.config.super_seeding = enabled;
3039 self.super_seed = if enabled {
3040 Some(crate::super_seed::SuperSeedState::new())
3041 } else {
3042 None
3043 };
3044 let _ = reply.send(());
3045 }
3046 Some(TorrentCommand::IsSuperSeeding { reply }) => {
3047 let _ = reply.send(self.config.super_seeding);
3048 }
3049 Some(TorrentCommand::SetSeedMode { enabled, reply }) => {
3050 self.handle_set_seed_mode(enabled);
3051 let _ = reply.send(());
3052 }
3053 Some(TorrentCommand::SetSeedRatioLimit { limit, reply }) => {
3054 self.config.seed_ratio_limit = limit;
3055 self.need_save_resume = true;
3056 let _ = reply.send(());
3057 }
3058 Some(TorrentCommand::AddTracker { url }) => {
3059 self.tracker_manager.add_tracker_url(&url);
3060 }
3061 Some(TorrentCommand::ReplaceTrackers { urls, reply }) => {
3062 self.tracker_manager.replace_all(&urls);
3063 let _ = reply.send(());
3064 }
3065 Some(TorrentCommand::ForceRecheck { reply }) => {
3066 self.handle_force_recheck(reply).await;
3067 }
3068 Some(TorrentCommand::RenameFile { file_index, new_name, reply }) => {
3069 let result = self.handle_rename_file(file_index, new_name).await;
3070 let _ = reply.send(result);
3071 }
3072 Some(TorrentCommand::SetMaxConnections { limit, reply }) => {
3073 self.max_connections = limit;
3074 let _ = reply.send(());
3075 }
3076 Some(TorrentCommand::MaxConnections { reply }) => {
3077 let _ = reply.send(self.max_connections);
3078 }
3079 Some(TorrentCommand::SetMaxUploads { limit, reply }) => {
3080 self.choker.set_unchoke_slots(limit);
3081 let _ = reply.send(());
3082 }
3083 Some(TorrentCommand::MaxUploads { reply }) => {
3084 let _ = reply.send(self.choker.unchoke_slots());
3085 }
3086 Some(TorrentCommand::GetPeerInfo { reply }) => {
3087 let _ = reply.send(self.build_peer_info());
3088 }
3089 Some(TorrentCommand::GetDownloadQueue { reply }) => {
3090 let _ = reply.send(self.build_download_queue());
3091 }
3092 Some(TorrentCommand::HavePiece { index, reply }) => {
3093 let has = self.chunk_tracker.as_ref()
3094 .is_some_and(|ct| ct.has_piece(index));
3095 let _ = reply.send(has);
3096 }
3097 Some(TorrentCommand::PieceAvailability { reply }) => {
3098 let mut avail = vec![0u32; self.num_pieces as usize];
3109 for peer in self.peers.values() {
3110 for i in 0..self.num_pieces {
3111 if peer.bitfield.get(i) {
3112 avail[i as usize] += 1;
3113 }
3114 }
3115 }
3116 let _ = reply.send(avail);
3117 }
3118 Some(TorrentCommand::FileProgress { reply }) => {
3119 let _ = reply.send(self.compute_file_progress());
3120 }
3121 Some(TorrentCommand::InfoHashes { reply }) => {
3122 let _ = reply.send(self.info_hashes.clone());
3123 }
3124 Some(TorrentCommand::TorrentFile { reply }) => {
3125 let _ = reply.send(self.meta.clone());
3126 }
3127 Some(TorrentCommand::TorrentFileV2 { reply }) => {
3128 let _ = reply.send(self.meta_v2.clone());
3129 }
3130 Some(TorrentCommand::ForceDhtAnnounce) => {
3131 self.handle_force_dht_announce().await;
3132 }
3133 Some(TorrentCommand::ReadPiece { index, reply }) => {
3134 let result = self.handle_read_piece(index).await;
3135 let _ = reply.send(result);
3136 }
3137 Some(TorrentCommand::FlushCache { reply }) => {
3138 let result = self.handle_flush_cache().await;
3139 let _ = reply.send(result);
3140 }
3141 Some(TorrentCommand::ClearError) => {
3142 self.handle_clear_error().await;
3143 }
3144 Some(TorrentCommand::ClearSaveResumeFlag) => {
3145 self.need_save_resume = false;
3146 }
3147 Some(TorrentCommand::MarkResumeDirty) => {
3148 self.need_save_resume = true;
3152 }
3153 Some(TorrentCommand::RestoreResumeBitmap { pieces, reply }) => {
3154 let result = self.handle_restore_resume_bitmap(pieces);
3155 let _ = reply.send(result);
3156 }
3157 Some(TorrentCommand::RestoreWebSeedStats { stats, reply }) => {
3158 self.web_seed_stats = stats;
3159 let _ = reply.send(Ok(()));
3160 }
3161 Some(TorrentCommand::GetPeerSourceCounts { reply }) => {
3162 let _ = reply.send((self.pex_peer_count, self.lsd_peer_count));
3163 }
3164 Some(TorrentCommand::QueryUnchokeDurations { reply }) => {
3165 let mut out = self.unchoke_durations.clone();
3166 let now = Instant::now();
3169 for peer in self.peers.values() {
3170 let mut delta = peer.unchoke_duration_total;
3171 if let Some(start) = peer.am_unchoke_started_at {
3172 delta += now.duration_since(start);
3173 }
3174 if !delta.is_zero() {
3175 *out.entry(peer.addr).or_default() += delta;
3176 }
3177 }
3178 let _ = reply.send(out);
3179 }
3180 Some(TorrentCommand::GetWebSeedStats { reply }) => {
3181 let snapshot: Vec<_> = self.web_seed_stats.values().cloned().collect();
3182 let _ = reply.send(snapshot);
3183 }
3184 Some(TorrentCommand::FileStatus { reply }) => {
3185 let _ = reply.send(self.build_file_status());
3186 }
3187 Some(TorrentCommand::Flags { reply }) => {
3188 let _ = reply.send(self.build_flags());
3189 }
3190 Some(TorrentCommand::SetFlags { flags, reply }) => {
3191 self.apply_set_flags(flags).await;
3192 let _ = reply.send(());
3193 }
3194 Some(TorrentCommand::UnsetFlags { flags, reply }) => {
3195 self.apply_unset_flags(flags).await;
3196 let _ = reply.send(());
3197 }
3198 Some(TorrentCommand::ConnectPeer { addr }) => {
3199 self.handle_connect_peer(addr);
3200 }
3201 Some(TorrentCommand::PreResolvedMetadata { info_bytes, peers }) => {
3202 self.handle_pre_resolved_metadata(info_bytes, peers).await;
3203 }
3204 #[cfg(feature = "test-util")]
3205 Some(TorrentCommand::TestInjectMetadata { info_bytes, reply }) => {
3206 self.handle_pre_resolved_metadata(info_bytes, vec![]).await;
3210 let _ = reply.send(());
3211 }
3212 Some(TorrentCommand::GetMeta { reply }) => {
3213 let _ = reply.send(self.meta.clone());
3218 }
3219 Some(TorrentCommand::UpdateSettings(delta)) => {
3220 self.handle_update_settings(&delta);
3221 }
3222 Some(TorrentCommand::Shutdown) => {
3223 info!("torrent actor: received Shutdown command, exiting");
3224 self.shutdown_web_seeds().await;
3225 self.shutdown_peers().await;
3226 return;
3227 }
3228 None => {
3229 warn!("torrent actor: cmd_rx channel closed (all senders dropped), exiting");
3230 self.shutdown_web_seeds().await;
3231 self.shutdown_peers().await;
3232 return;
3233 }
3234 }
3235 }
3236 Some(err) = self.write_error_rx.recv() => {
3238 warn!(piece = err.piece, begin = err.begin, "async disk write failed: {}", err.error);
3239 }
3240 result = accept_incoming(&mut self.listener) => {
3242 if let Ok((stream, addr)) = result {
3243 self.spawn_peer_from_stream(addr, stream);
3244 }
3245 }
3246 stream = accept_i2p(&mut self.i2p_accept_rx) => {
3248 if let Some(stream) = stream {
3249 self.handle_i2p_incoming(stream);
3250 }
3251 }
3252 _ = rate_interval.tick() => {
3255 self.update_peer_rates();
3256 }
3257 _ = unchoke_interval.tick() => {
3259 if self.state == TorrentState::Seeding
3268 || self.state == TorrentState::Sharing
3269 {
3270 self.slot_tuner.observe(self.upload_bytes_interval);
3271 self.choker.observe_throughput(self.upload_bytes_interval);
3272 self.upload_bytes_interval = 0;
3273 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3274 self.run_choker().await;
3275 self.force_immediate_choker_tick = false;
3278 } else {
3279 self.upload_bytes_interval = 0;
3280 }
3281 self.update_streaming_cursors();
3283 if self.config.auto_sequential {
3285 self.auto_sequential_active = crate::piece_selector::evaluate_auto_sequential(
3286 self.piece_owner.iter().filter(|o| o.is_some()).count(),
3287 self.peers.len(),
3288 self.auto_sequential_active,
3289 );
3290 }
3291 self.assign_pieces_to_web_seeds();
3293 }
3294 _ = optimistic_interval.tick() => {
3296 self.rotate_optimistic();
3297 }
3298 Some(connect_peer) = async {
3300 match self.connect_rx.as_mut() {
3301 Some(rx) => rx.recv().await,
3302 None => std::future::pending().await,
3303 }
3304 } => {
3305 self.handle_adder_connect(connect_peer);
3306 }
3307 () = &mut dht_requery_sleep, if self.state != TorrentState::Complete
3308 && self.state != TorrentState::Paused
3309 && self.state != TorrentState::Queued
3310 && self.state != TorrentState::Seeding
3311 && self.state != TorrentState::Stopped => {
3312 self.run_dht_requery().await;
3313 dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::from_mins(1)));
3314 }
3315 () = async {
3320 match self.tracker_manager.next_announce_in() {
3321 Some(dur) => tokio::time::sleep(dur).await,
3322 None => std::future::pending().await,
3323 }
3324 }, if self.tracker_result_rx.is_none() => {
3325 let left = self.calculate_left();
3326 self.tracker_result_rx = Some(self.tracker_manager.start_announce(
3327 irontide_tracker::AnnounceEvent::None,
3328 self.uploaded,
3329 self.downloaded,
3330 left,
3331 ));
3332 }
3333 result = async {
3336 match self.tracker_result_rx.as_mut() {
3337 Some(rx) => rx.recv().await,
3338 None => std::future::pending().await,
3339 }
3340 } => {
3341 match result {
3342 Some(batch) => {
3343 let (peers, outcome) = self.tracker_manager.process_tracker_result(batch);
3344 self.fire_tracker_alerts(&[outcome]);
3345 if !peers.is_empty() {
3346 debug!(count = peers.len(), "tracker returned peers (streaming)");
3347 self.handle_add_peers(peers, PeerSource::Tracker);
3348 }
3349 }
3350 None => {
3351 self.tracker_result_rx = None;
3354 }
3355 }
3356 }
3357 result = async {
3359 match &mut self.dht_peers_rx {
3360 Some(rx) => rx.recv().await,
3361 None => std::future::pending().await,
3362 }
3363 } => {
3364 if let Some(peers) = result {
3365 debug!(count = peers.len(), "DHT v4 returned peers");
3366 self.handle_add_peers(peers, PeerSource::Dht);
3367 } else {
3368 debug!("DHT v4 peer search exhausted");
3369 self.dht_peers_rx = None;
3370 }
3371 }
3372 result = async {
3374 match &mut self.dht_v6_peers_rx {
3375 Some(rx) => rx.recv().await,
3376 None => std::future::pending().await,
3377 }
3378 } => {
3379 if let Some(peers) = result {
3380 debug!(count = peers.len(), "DHT v6 returned peers");
3381 self.dht_v6_empty_count = 0; self.handle_add_peers(peers, PeerSource::Dht);
3383 } else {
3384 self.dht_v6_peers_rx = None;
3385 self.dht_v6_empty_count += 1;
3386 if self.dht_v6_empty_count == 30 {
3387 debug!("DHT v6 routing table persistently empty, giving up");
3388 } else if self.dht_v6_empty_count < 30 {
3389 debug!("DHT v6 peer search exhausted");
3390 }
3391 }
3392 }
3393 result = async {
3395 match &mut self.dht_v2_peers_rx {
3396 Some(rx) => rx.recv().await,
3397 None => std::future::pending().await,
3398 }
3399 } => {
3400 if let Some(peers) = result {
3401 debug!(count = peers.len(), "DHT v4 v2-swarm returned peers");
3402 self.handle_add_peers(peers, PeerSource::Dht);
3403 } else {
3404 debug!("DHT v4 v2-swarm peer search exhausted");
3405 self.dht_v2_peers_rx = None;
3406 }
3407 }
3408 result = async {
3410 match &mut self.dht_v6_v2_peers_rx {
3411 Some(rx) => rx.recv().await,
3412 None => std::future::pending().await,
3413 }
3414 } => {
3415 if let Some(peers) = result {
3416 debug!(count = peers.len(), "DHT v6 v2-swarm returned peers");
3417 self.handle_add_peers(peers, PeerSource::Dht);
3418 } else {
3419 debug!("DHT v6 v2-swarm peer search exhausted");
3420 self.dht_v6_v2_peers_rx = None;
3421 }
3422 }
3423 _ = async {
3425 match suggest_interval {
3426 Some(ref mut interval) => interval.tick().await,
3427 None => std::future::pending().await,
3428 }
3429 } => {
3430 self.suggest_cached_pieces().await;
3431 }
3432 _ = turnover_interval.tick() => {
3433 self.run_steal_queue_maintenance();
3434 }
3435 _ = pipeline_tick_interval.tick() => {
3437 let snub_timeout = Duration::from_secs(u64::from(self.config.snub_timeout_secs));
3438
3439 for (_addr, peer) in &mut self.peers {
3440 peer.pipeline.tick();
3441
3442 if !peer.peer_choking && !peer.snubbed {
3444 let idle = peer.last_data_received
3445 .is_some_and(|t| t.elapsed() > snub_timeout);
3446 if idle {
3447 peer.snubbed = true;
3448 peer.blocks_timed_out = peer.blocks_timed_out
3450 .saturating_add(peer.pending_requests.len() as u64);
3451 debug!(%_addr, "peer snubbed (no data for {}s)", self.config.snub_timeout_secs);
3452 }
3453 }
3454 }
3455
3456 self.refresh_peer_rates();
3459
3460 if !self.end_game.is_active() {
3462 self.check_end_game_activation();
3463 }
3464
3465 self.tick_dispatch_safety_wake();
3466
3467 if self.config.choke_rotation_max_evictions > 0
3469 && self.state == TorrentState::Downloading
3470 {
3471 self.run_choke_rotation();
3472 }
3473
3474 if self.order_map_dirty {
3480 self.rebuild_order_map_now();
3481 }
3482 }
3483 _ = end_game_tick_interval.tick(), if self.end_game.is_active() => {
3488 let addrs: Vec<SocketAddr> = self.peers.iter()
3489 .filter(|(_, p)| !p.peer_choking && p.pending_requests.len() < END_GAME_DEPTH)
3490 .map(|(addr, _)| *addr)
3491 .collect();
3492 for addr in addrs {
3493 self.request_end_game_block(addr).await;
3494 }
3495 }
3496 _ = metadata_timeout_interval.tick(), if self.state == TorrentState::FetchingMetadata => {
3499 let timed_out: Vec<u32> = self
3501 .metadata_downloader
3502 .as_ref()
3503 .map(MetadataDownloader::timed_out_pieces)
3504 .unwrap_or_default();
3505
3506 if !timed_out.is_empty() {
3507 debug!(count = timed_out.len(), "metadata pieces timed out, re-requesting");
3508
3509 let eligible_senders: Vec<mpsc::Sender<PeerCommand>> = self
3512 .peers
3513 .iter()
3514 .filter(|(addr, peer)| {
3515 self.metadata_downloader
3516 .as_ref()
3517 .is_some_and(|dl| !dl.is_rejected(addr))
3518 && peer
3519 .ext_handshake
3520 .as_ref()
3521 .is_some_and(|h| h.metadata_size.is_some())
3522 })
3523 .map(|(_, peer)| peer.cmd_tx.clone())
3524 .collect();
3525
3526 for cmd_tx in &eligible_senders {
3528 for &piece in &timed_out {
3529 let _ = cmd_tx.try_send(PeerCommand::RequestMetadata { piece });
3530 }
3531 }
3532
3533 if let Some(ref mut dl) = self.metadata_downloader {
3535 for piece in timed_out {
3536 dl.reset_request_time(piece);
3537 }
3538 }
3539 }
3540 }
3541 _ = diag_interval.tick() => {
3543 {
3545 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3546 let eg = self.end_game.is_active();
3547 let eg_blocks = self.end_game.block_count();
3548 info!(state = ?self.state, have, total = self.num_pieces, end_game = eg, eg_blocks, "heartbeat");
3549 }
3550 if self.state == TorrentState::Downloading {
3551 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3552 let in_flight = self.atomic_states.as_ref().map_or(0, |s| s.in_flight_count() as usize);
3553 let unchoked = self.peers.values().filter(|p| !p.peer_choking).count();
3554 info!(have, in_flight, total = self.num_pieces,
3555 downloaded_mb = self.downloaded / (1024 * 1024),
3556 peers = self.peers.len(), unchoked,
3557 "download progress");
3558 for (addr, p) in &self.peers {
3559 let last_data = p.last_data_received.map_or(9999, |t| t.elapsed().as_secs());
3560 trace!(%addr,
3561 choking = p.peer_choking,
3562 pending = p.pending_requests.len(),
3563 ewma_rate = p.pipeline.ewma_rate() as u64,
3564 last_data_secs = last_data,
3565 bf_ones = p.bitfield.count_ones(),
3566 "peer state");
3567 }
3568 }
3569 }
3570 _ = conn_stats_interval.tick() => {
3572 if self.connect_attempts > 0 {
3573 let succeeded = self.connect_attempts.saturating_sub(self.connect_failures);
3574 let success_pct = (succeeded as f64 / self.connect_attempts as f64 * 100.0) as u32;
3575 info!(
3576 connected = self.peers.len(),
3577 attempted = self.connect_attempts,
3578 failed = self.connect_failures,
3579 success_rate = %format!("{success_pct}%"),
3580 "connection stats"
3581 );
3582 }
3583 }
3584 _ = soft_reap_interval.tick() => {
3590 let soft_timeout = self.config.connect_soft_timeout;
3591 if soft_timeout > 0 {
3592 if let Some(ref ps) = self.peer_states {
3593 ps.soft_reap_candidates_into(
3594 Duration::from_secs(soft_timeout),
3595 &mut self.soft_reap_buf,
3596 );
3597 } else {
3598 self.soft_reap_buf.clear();
3599 }
3600 for i in 0..self.soft_reap_buf.len() {
3601 let peer_addr = self.soft_reap_buf[i];
3602 debug!(%peer_addr, soft_timeout, "soft reap: no TCP SYN-ACK");
3603 self.connect_permits.remove(&peer_addr);
3605 self.disconnect_peer(peer_addr, "soft reap: no TCP SYN-ACK");
3606 if let Some(ref ps) = self.peer_states
3607 && let Some(backoff) = ps.mark_dead(peer_addr)
3608 {
3609 let ps_clone = Arc::clone(ps);
3610 tokio::spawn(async move {
3611 tokio::time::sleep(backoff).await;
3612 ps_clone.mark_queued_for_retry(peer_addr);
3613 });
3614 }
3615 }
3616 self.soft_reap_buf.clear();
3617 }
3618 }
3619 _ = eviction_interval.tick() => {
3642 if self.force_immediate_choker_tick
3648 && (self.state == TorrentState::Seeding
3649 || self.state == TorrentState::Sharing)
3650 {
3651 self.slot_tuner.observe(self.upload_bytes_interval);
3652 self.choker.observe_throughput(self.upload_bytes_interval);
3653 self.upload_bytes_interval = 0;
3654 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3655 self.run_choker().await;
3656 self.force_immediate_choker_tick = false;
3657 }
3658 if self.state != TorrentState::Seeding {
3659 let prune_cutoff = std::time::Duration::from_mins(1);
3662 while self
3663 .eviction_history
3664 .front()
3665 .copied()
3666 .is_some_and(|t| t.elapsed() > prune_cutoff)
3667 {
3668 self.eviction_history.pop_front();
3669 }
3670 let limit = self.config.proactive_evictions_per_minute_limit as usize;
3671 let window_ok = self.eviction_history.len() < limit;
3672
3673 let should_evict = window_ok
3677 && self.peer_states.as_ref().is_some_and(|ps| {
3678 let live = ps
3679 .stats
3680 .live
3681 .load(std::sync::atomic::Ordering::Relaxed);
3682 #[allow(
3683 clippy::cast_possible_truncation,
3684 clippy::cast_sign_loss
3685 )]
3686 let threshold =
3687 (self.effective_max_connections() as f32 * 0.95) as u32;
3688 debug_assert!(
3689 self.effective_max_connections()
3690 <= crate::torrent_peers::HARD_PEER_CEILING,
3691 "effective_max must be clamped to HARD_PEER_CEILING"
3692 );
3693 live >= threshold
3694 });
3695 if should_evict {
3696 let max_this_tick = 5.min(limit.saturating_sub(self.eviction_history.len()));
3699 for _ in 0..max_this_tick {
3700 match self.find_eviction_candidate() {
3701 Some((victim, pass)) => {
3702 debug!(%victim, ?pass, "v0.187.3 proactive eviction");
3703 self.disconnect_peer(victim, "proactive eviction");
3704 if matches!(pass, crate::torrent_peers::EvictionPass::ZeroThroughput)
3705 && let Some(ref ps) = self.peer_states
3706 {
3707 ps.add_eviction_ban(victim);
3708 }
3709 self.eviction_history.push_back(std::time::Instant::now());
3710 }
3711 None => break,
3712 }
3713 }
3714 }
3715
3716 self.run_piece_steal_scan();
3718 }
3719 }
3720 _ = refill_interval.tick() => {
3722 let elapsed = Duration::from_millis(100);
3723 self.upload_bucket.refill(elapsed);
3724 self.download_bucket.lock().refill(elapsed);
3725 self.rate_limiter_set.refill(elapsed);
3727 let (tcp_peers, utp_peers) = self.transport_peer_counts();
3728 self.rate_limiter_set.apply_mixed_mode(
3729 self.config.mixed_mode_algorithm,
3730 tcp_peers,
3731 utp_peers,
3732 self.config.upload_rate_limit,
3733 );
3734 }
3735 }
3736
3737 for target in std::mem::take(&mut self.holepunch_pending) {
3739 self.try_holepunch(target).await;
3740 }
3741 }
3742 }
3743
3744 pub(crate) fn distributed_copies(&self) -> (u32, u32, f32) {
3750 if self.num_pieces == 0 || self.peers.is_empty() {
3751 return (0, 0, 0.0);
3752 }
3753
3754 let num = self.num_pieces as usize;
3755 let mut availability = vec![0u32; num];
3756
3757 for peer in self.peers.values() {
3758 for idx in 0..self.num_pieces {
3759 if peer.bitfield.get(idx) {
3760 availability[idx as usize] += 1;
3761 }
3762 }
3763 }
3764
3765 let min_avail = availability.iter().copied().min().unwrap_or(0);
3766 let rarest_count = availability.iter().filter(|&&c| c == min_avail).count() as u32;
3767 let fraction = ((self.num_pieces - rarest_count) * 1000) / self.num_pieces;
3768 let copies_float = min_avail as f32 + fraction as f32 / 1000.0;
3769
3770 (min_avail, fraction, copies_float)
3771 }
3772
3773 fn build_download_queue(&self) -> Vec<PartialPieceInfo> {
3777 self.piece_owner
3778 .iter()
3779 .enumerate()
3780 .filter_map(|(piece_index, owner)| {
3781 owner.map(|_| {
3782 let piece_index = piece_index as u32;
3783 let blocks_in_piece = self
3784 .lengths
3785 .as_ref()
3786 .map_or(0, |l| l.piece_size(piece_index).div_ceil(l.chunk_size()));
3787 PartialPieceInfo {
3788 piece_index,
3789 blocks_in_piece,
3790 blocks_assigned: 0,
3791 }
3792 })
3793 })
3794 .collect()
3795 }
3796
3797 fn compute_file_progress(&self) -> Vec<u64> {
3803 let Some(meta) = self.meta.as_ref() else {
3804 return Vec::new();
3805 };
3806 let Some(lengths) = self.lengths.as_ref() else {
3807 return Vec::new();
3808 };
3809 let Some(chunk_tracker) = self.chunk_tracker.as_ref() else {
3810 return Vec::new();
3811 };
3812
3813 let files = meta.info.files();
3814 if files.is_empty() {
3815 return Vec::new();
3816 }
3817
3818 let piece_length = lengths.piece_length();
3819 let mut result = Vec::with_capacity(files.len());
3820 let mut file_offset = 0u64;
3821
3822 for file_entry in &files {
3823 let file_len = file_entry.length;
3824 if file_len == 0 {
3825 result.push(0);
3826 file_offset += file_len;
3827 continue;
3828 }
3829
3830 let file_end = file_offset + file_len;
3831 let first_piece = (file_offset / piece_length) as u32;
3832 let last_piece = ((file_end - 1) / piece_length) as u32;
3833
3834 let mut downloaded = 0u64;
3835
3836 for p in first_piece..=last_piece {
3837 if !chunk_tracker.has_piece(p) {
3838 continue;
3839 }
3840
3841 let piece_start = lengths.piece_offset(p);
3842 let piece_end = piece_start + u64::from(lengths.piece_size(p));
3843
3844 let overlap_start = piece_start.max(file_offset);
3846 let overlap_end = piece_end.min(file_end);
3847
3848 if overlap_start < overlap_end {
3849 downloaded += overlap_end - overlap_start;
3850 }
3851 }
3852
3853 result.push(downloaded);
3854 file_offset = file_end;
3855 }
3856
3857 result
3858 }
3859
3860 fn v6_retry_delay(&self) -> std::time::Duration {
3863 let base_ms: u64 = 100;
3864 let max_ms: u64 = 5000;
3865 let delay_ms = base_ms
3866 .saturating_mul(
3867 1u64.checked_shl(self.dht_v6_empty_count)
3868 .unwrap_or(u64::MAX),
3869 )
3870 .min(max_ms);
3871 std::time::Duration::from_millis(delay_ms)
3872 }
3873
3874 fn should_retry_v6(&self) -> bool {
3876 let Some(last) = self.dht_v6_last_retry else {
3877 return true; };
3879 last.elapsed() >= self.v6_retry_delay()
3880 }
3881
3882 async fn handle_force_dht_announce(&self) {
3884 if let Some(dht) = self.current_dht()
3885 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
3886 {
3887 warn!("Force DHT v4 announce failed: {e}");
3888 }
3889 if let Some(dht6) = self.current_dht_v6()
3890 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
3891 {
3892 debug!("Force DHT v6 announce failed: {e}");
3893 }
3894 if self.info_hashes.is_hybrid()
3896 && let Some(v2) = self.info_hashes.v2
3897 {
3898 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
3899 if v2_as_v1 != self.info_hash {
3900 if let Some(dht) = self.current_dht()
3901 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
3902 {
3903 debug!("Force DHT v4 dual-swarm announce failed: {e}");
3904 }
3905 if let Some(dht6) = self.current_dht_v6()
3906 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
3907 {
3908 debug!("Force DHT v6 dual-swarm announce failed: {e}");
3909 }
3910 }
3911 }
3912 }
3913
3914 async fn run_dht_requery(&mut self) {
3920 if !self.config.enable_dht {
3921 return;
3922 }
3923
3924 if self.peers.len() > self.config.max_peers.saturating_mul(4) {
3928 return;
3929 }
3930
3931 if self.dht_peers_rx.is_none()
3939 && let Some(dht) = self.current_dht()
3940 {
3941 match dht.get_peers(self.info_hash).await {
3942 Ok(rx) => self.dht_peers_rx = Some(rx),
3943 Err(e) => warn!("DHT v4 re-query failed: {e}"),
3944 }
3945 }
3946
3947 if self.dht_v6_peers_rx.is_none()
3949 && self.dht_v6_empty_count < 30
3950 && self.should_retry_v6()
3951 && let Some(dht6) = self.current_dht_v6()
3952 {
3953 self.dht_v6_last_retry = Some(std::time::Instant::now());
3954 match dht6.get_peers(self.info_hash).await {
3955 Ok(rx) => self.dht_v6_peers_rx = Some(rx),
3956 Err(e) => debug!("DHT v6 re-query failed: {e}"),
3957 }
3958 }
3959
3960 if self.info_hashes.is_hybrid()
3962 && let Some(v2) = self.info_hashes.v2
3963 {
3964 let v2_bytes: [u8; 20] = v2.0[..20]
3965 .try_into()
3966 .expect("Id32 is 32 bytes; first 20 always fit");
3967 let v2_as_v1 = Id20(v2_bytes);
3968
3969 if self.dht_v2_peers_rx.is_none()
3970 && let Some(dht) = self.current_dht()
3971 {
3972 match dht.get_peers(v2_as_v1).await {
3973 Ok(rx) => self.dht_v2_peers_rx = Some(rx),
3974 Err(e) => debug!("DHT v4 v2-swarm re-query failed: {e}"),
3975 }
3976 }
3977 if self.dht_v6_v2_peers_rx.is_none()
3978 && self.dht_v6_empty_count < 30
3979 && self.should_retry_v6()
3980 && let Some(dht6) = self.current_dht_v6()
3981 {
3982 self.dht_v6_last_retry = Some(std::time::Instant::now());
3983 match dht6.get_peers(v2_as_v1).await {
3984 Ok(rx) => self.dht_v6_v2_peers_rx = Some(rx),
3985 Err(e) => debug!("DHT v6 v2-swarm re-query failed: {e}"),
3986 }
3987 }
3988 }
3989
3990 debug!(peers = self.peers.len(), "DHT re-query triggered");
3991 }
3992
3993 async fn handle_read_piece(&self, index: u32) -> crate::Result<Bytes> {
3995 let disk = self
3996 .disk
3997 .as_ref()
3998 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3999 let lengths = self
4000 .lengths
4001 .as_ref()
4002 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4003
4004 let piece_size = lengths.piece_size(index);
4005 if piece_size == 0 {
4006 return Err(crate::Error::InvalidPieceIndex {
4007 index,
4008 num_pieces: lengths.num_pieces(),
4009 });
4010 }
4011
4012 let chunk_size = lengths.chunk_size();
4013 let num_chunks = lengths.chunks_in_piece(index);
4014 let mut buf = bytes::BytesMut::with_capacity(piece_size as usize);
4015
4016 for chunk_idx in 0..num_chunks {
4017 let begin = chunk_idx * chunk_size;
4018 let len = if chunk_idx == num_chunks - 1 {
4019 piece_size - begin
4020 } else {
4021 chunk_size
4022 };
4023 let data = disk
4024 .read_chunk(index, begin, len, DiskJobFlags::empty())
4025 .await
4026 .map_err(crate::Error::Storage)?;
4027 buf.extend_from_slice(&data);
4028 }
4029
4030 Ok(buf.freeze())
4031 }
4032
4033 async fn handle_flush_cache(&self) -> crate::Result<()> {
4035 let disk = self
4036 .disk
4037 .as_ref()
4038 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4039 disk.flush_cache().await.map_err(crate::Error::Storage)
4040 }
4041
4042 fn handle_connect_peer(&mut self, addr: SocketAddr) {
4044 if self.peers.contains_key(&addr) {
4046 return;
4047 }
4048 if let Some(ref ps) = self.peer_states {
4050 ps.add_if_not_seen(addr, PeerSource::Incoming);
4051 }
4052 }
4053
4054 pub(crate) fn fire_tracker_alerts(&self, outcomes: &[crate::tracker_manager::TrackerOutcome]) {
4056 for outcome in outcomes {
4057 match &outcome.result {
4058 Ok(num_peers) => {
4059 post_alert(
4060 &self.alert_tx,
4061 &self.alert_mask,
4062 AlertKind::TrackerReply {
4063 info_hash: self.info_hash,
4064 url: outcome.url.clone(),
4065 num_peers: *num_peers,
4066 },
4067 );
4068 }
4069 Err(msg) => {
4070 post_alert(
4071 &self.alert_tx,
4072 &self.alert_mask,
4073 AlertKind::TrackerError {
4074 info_hash: self.info_hash,
4075 url: outcome.url.clone(),
4076 message: msg.clone(),
4077 },
4078 );
4079 }
4080 }
4081 }
4082 }
4083
4084 pub(crate) fn calculate_left(&self) -> u64 {
4086 match (&self.meta, &self.chunk_tracker) {
4087 (Some(meta), Some(ct)) => {
4088 let total = meta.info.total_length();
4089 let have = u64::from(ct.bitfield().count_ones());
4090 let pieces_total = u64::from(self.num_pieces);
4091 let per_piece = total.checked_div(pieces_total).unwrap_or(0);
4092 total.saturating_sub(have * per_piece)
4093 }
4094 _ => 0,
4095 }
4096 }
4097
4098 pub(crate) async fn shutdown_peers(&mut self) {
4099 let left = self.calculate_left();
4101 let _ = tokio::time::timeout(
4102 std::time::Duration::from_secs(3),
4103 self.tracker_manager
4104 .announce_stopped(self.uploaded, self.downloaded, left),
4105 )
4106 .await;
4107
4108 for peer in self.peers.values() {
4110 let _ = peer.cmd_tx.try_send(PeerCommand::Shutdown);
4111 }
4112 }
4113
4114 pub(crate) async fn handle_piece_data(
4117 &mut self,
4118 peer_addr: SocketAddr,
4119 index: u32,
4120 begin: u32,
4121 data: Bytes,
4122 ) {
4123 if let Some(ref ct) = self.chunk_tracker
4127 && ct.has_chunk(index, begin)
4128 {
4129 self.total_download += data.len() as u64 + 13;
4130 if let Some(peer) = self.peers.get_mut(&peer_addr) {
4134 peer.pending_requests.remove(index, begin);
4135 }
4136 if self.end_game.is_active() {
4140 self.end_game.block_received(index, begin, peer_addr);
4141 }
4142 return;
4144 }
4145
4146 let data_len = data.len();
4147
4148 if let Some(ref disk) = self.disk {
4150 disk.write_block_deferred(index, begin, data);
4151 }
4152
4153 self.downloaded += data_len as u64;
4154 self.total_download += data_len as u64 + 13; self.last_download = now_unix();
4156 self.need_save_resume = true;
4157
4158 if let Some(slab_idx) = self.peer_slab.slot_of(&peer_addr)
4160 && self.piece_owner.get(index as usize) == Some(&None)
4161 {
4162 self.piece_owner[index as usize] = Some(slab_idx);
4163 if self.inflight_started.get(index as usize) == Some(&None) {
4165 self.inflight_started[index as usize] = Some(Instant::now());
4166 }
4167 if let (Some(sc), Some(bm)) = (&self.steal_candidates, &self.block_maps)
4169 && let Some(lengths) = &self.lengths
4170 {
4171 let total_blocks = lengths.chunks_in_piece(index);
4172 if bm.next_unrequested(index, total_blocks).is_some() {
4173 sc.push(index);
4174 }
4175 }
4176 }
4177
4178 self.piece_contributors
4180 .entry(index)
4181 .or_default()
4182 .insert(peer_addr.ip());
4183
4184 let now = std::time::Instant::now();
4185 if let Some(peer) = self.peers.get_mut(&peer_addr) {
4186 peer.pending_requests.remove(index, begin);
4187 peer.download_bytes_window += data_len as u64;
4188 peer.download_bytes_total += data_len as u64;
4189 peer.pipeline
4190 .block_received(index, begin, data_len as u32, now);
4191 peer.last_data_received = Some(now);
4192 if peer.snubbed {
4194 peer.snubbed = false;
4195 }
4196 }
4197 if self.end_game.is_active() {
4202 let cancels = self.end_game.block_received(index, begin, peer_addr);
4203 for (cancel_addr, ci, cb, cl) in cancels {
4204 if let Some(cancel_peer) = self.peers.get_mut(&cancel_addr) {
4205 let _ = cancel_peer.cmd_tx.try_send(PeerCommand::Cancel {
4206 index: ci,
4207 begin: cb,
4208 length: cl,
4209 });
4210 cancel_peer.pending_requests.remove(ci, cb);
4211 }
4212 }
4213 }
4214
4215 let piece_complete = if let Some(ref mut ct) = self.chunk_tracker {
4217 ct.chunk_received(index, begin)
4218 } else {
4219 false
4220 };
4221
4222 if piece_complete && !self.pending_verify.contains(&index) {
4223 if self.config.predictive_piece_announce_ms > 0
4225 && !self.predictive_have_sent.contains(&index)
4226 {
4227 self.predictive_have_sent.insert(index);
4228 let _ = self.have_broadcast_tx.send(index);
4229 }
4230
4231 if let Some(ref disk) = self.disk {
4234 disk.flush_piece_writes(index).await;
4235 }
4236
4237 match self.version {
4238 irontide_core::TorrentVersion::V1Only => {
4239 if let Some(ref disk) = self.disk
4241 && let Some(expected) = self
4242 .meta
4243 .as_ref()
4244 .and_then(|m| m.info.piece_hash(index as usize))
4245 {
4246 self.pending_verify.insert(index);
4247 let generation = self
4248 .piece_generations
4249 .get(index as usize)
4250 .copied()
4251 .unwrap_or(0);
4252 disk.enqueue_verify(index, expected, generation, &self.verify_result_tx);
4253 }
4254 }
4255 irontide_core::TorrentVersion::V2Only => {
4256 self.verify_and_mark_piece_v2(index).await;
4258 }
4259 irontide_core::TorrentVersion::Hybrid => {
4260 self.verify_and_mark_piece_hybrid(index).await;
4262 }
4263 }
4264 }
4265
4266 if self.end_game.is_active() {
4269 self.request_end_game_block(peer_addr).await;
4270 }
4271 }
4272
4273 pub(crate) async fn handle_piece_blocks_batch(
4278 &mut self,
4279 peer_addr: SocketAddr,
4280 blocks: Vec<crate::types::BlockEntry>,
4281 ) {
4282 for block in &blocks {
4283 self.process_block_completion(peer_addr, block.index, block.begin, block.length)
4284 .await;
4285 }
4286 }
4287
4288 fn handle_open_file(
4289 &mut self,
4290 file_index: usize,
4291 ) -> crate::Result<crate::streaming::FileStreamHandle> {
4292 let meta = self
4293 .meta
4294 .as_ref()
4295 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4296 let files = meta.info.files();
4297 if file_index >= files.len() {
4298 return Err(crate::Error::InvalidFileIndex {
4299 index: file_index,
4300 count: files.len(),
4301 });
4302 }
4303 if self.file_priorities.get(file_index).copied() == Some(FilePriority::Skip) {
4304 return Err(crate::Error::FileSkipped { index: file_index });
4305 }
4306
4307 let lengths = self
4308 .lengths
4309 .as_ref()
4310 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4311 let disk = self
4312 .disk
4313 .as_ref()
4314 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4315
4316 let mut file_offset = 0u64;
4318 for f in &files[..file_index] {
4319 file_offset += f.length;
4320 }
4321 let file_length = files[file_index].length;
4322
4323 let (cursor_tx, cursor_rx) = tokio::sync::watch::channel(0u64);
4324
4325 let permit = self
4326 .stream_read_semaphore
4327 .clone()
4328 .try_acquire_owned()
4329 .map_err(|_| crate::Error::Connection("too many concurrent stream readers".into()))?;
4330
4331 self.streaming_cursors
4333 .push(crate::streaming::StreamingCursor {
4334 file_index,
4335 file_offset,
4336 cursor_piece: (file_offset / lengths.piece_length()) as u32,
4337 readahead_pieces: self.config.readahead_pieces,
4338 cursor_rx,
4339 });
4340
4341 Ok(crate::streaming::FileStreamHandle {
4342 disk: disk.clone(),
4343 lengths: lengths.clone(),
4344 file_index,
4345 file_offset,
4346 file_length,
4347 cursor_tx,
4348 piece_ready_rx: self.piece_ready_tx.subscribe(),
4349 have: self.have_watch_rx.clone(),
4350 read_permit: permit,
4351 })
4352 }
4353
4354 async fn suggest_cached_pieces(&mut self) {
4356 if !self.config.suggest_mode {
4357 return;
4358 }
4359 let disk = match self.disk {
4360 Some(ref d) => d.clone(),
4361 None => return,
4362 };
4363 let cached = disk.cached_pieces().await;
4364 if cached.is_empty() {
4365 return;
4366 }
4367 let max_suggest = self.config.max_suggest_pieces;
4368 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4369 for peer_addr in peer_addrs {
4370 let already_suggested = self.suggested_to_peers.entry(peer_addr).or_default();
4371 let peer_has_piece = |piece: u32| -> bool {
4372 self.peers
4373 .get(&peer_addr)
4374 .is_some_and(|p| p.bitfield.get(piece))
4375 };
4376 let mut sent = 0;
4377 for &piece in &cached {
4378 if sent >= max_suggest {
4379 break;
4380 }
4381 if peer_has_piece(piece) {
4382 continue;
4383 }
4384 if already_suggested.contains(&piece) {
4385 continue;
4386 }
4387 if let Some(peer) = self.peers.get(&peer_addr) {
4388 let _ = peer.cmd_tx.try_send(PeerCommand::SuggestPiece(piece));
4389 already_suggested.insert(piece);
4390 sent += 1;
4391 }
4392 }
4393 }
4394 }
4395
4396 async fn handle_pre_resolved_metadata(&mut self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
4402 if self.state != TorrentState::FetchingMetadata {
4404 debug!(
4405 info_hash = %self.info_hash,
4406 state = ?self.state,
4407 "ignoring pre-resolved metadata: already past FetchingMetadata"
4408 );
4409 return;
4410 }
4411
4412 debug!(
4413 info_hash = %self.info_hash,
4414 info_bytes_len = info_bytes.len(),
4415 num_peers = peers.len(),
4416 "received pre-resolved metadata from background resolver"
4417 );
4418
4419 if let Some(ref mut dl) = self.metadata_downloader {
4421 dl.set_total_size(info_bytes.len() as u64);
4423
4424 let piece_size: usize = 16384;
4428 let num_pieces = info_bytes.len().div_ceil(piece_size);
4429 for i in 0..num_pieces {
4430 let start = i * piece_size;
4431 let end = (start + piece_size).min(info_bytes.len());
4432 let data = bytes::Bytes::copy_from_slice(&info_bytes[start..end]);
4433 dl.piece_received(i as u32, data);
4434 }
4435 }
4436
4437 self.try_assemble_metadata().await;
4440
4441 if !peers.is_empty() {
4443 self.handle_add_peers(peers, crate::peer_state::PeerSource::Dht);
4444 }
4445 }
4446
4447 pub(crate) async fn try_assemble_metadata(&mut self) {
4448 let assembled = if let Some(ref dl) = self.metadata_downloader {
4449 dl.assemble_and_verify()
4450 } else {
4451 return;
4452 };
4453
4454 match assembled {
4455 Ok(info_bytes) => {
4456 let mut torrent_bytes = b"d4:info".to_vec();
4462 torrent_bytes.extend_from_slice(&info_bytes);
4463 torrent_bytes.push(b'e');
4464
4465 match torrent_from_bytes(&torrent_bytes) {
4466 Ok(meta) => {
4467 let num_pieces = meta.info.num_pieces() as u32;
4468 let lengths = Lengths::new(
4469 meta.info.total_length(),
4470 meta.info.piece_length,
4471 DEFAULT_CHUNK_SIZE,
4472 );
4473
4474 let files = meta.info.files();
4476 let file_paths: Vec<std::path::PathBuf> = files
4477 .iter()
4478 .map(|f| f.path.iter().collect::<std::path::PathBuf>())
4479 .collect();
4480 let file_lengths_vec: Vec<u64> = files.iter().map(|f| f.length).collect();
4481 let prealloc_mode = self.config.preallocate_mode.unwrap_or_else(|| {
4482 irontide_storage::PreallocateMode::from(
4483 self.config.storage_mode == irontide_core::StorageMode::Full,
4484 )
4485 });
4486 let storage: Arc<dyn TorrentStorage> =
4487 match irontide_storage::FilesystemStorage::new(
4488 &self.config.download_dir,
4489 file_paths,
4490 file_lengths_vec,
4491 lengths.clone(),
4492 None,
4493 prealloc_mode,
4494 self.config.filesystem_direct_io,
4495 ) {
4496 Ok(s) => Arc::new(s),
4497 Err(e) => {
4498 warn!(
4499 "failed to create filesystem storage: {e}, falling back to memory"
4500 );
4501 Arc::new(MemoryStorage::new(lengths.clone()))
4502 }
4503 };
4504 let mut disk_handle = self
4505 .disk_manager
4506 .register_torrent(self.info_hash, storage)
4507 .await;
4508
4509 self.chunk_tracker = Some(ChunkTracker::new(lengths.clone()));
4510 self.lengths = Some(lengths);
4511 self.num_pieces = num_pieces;
4512 self.piece_generations = vec![0u64; num_pieces as usize];
4514 let (hash_tx, hash_rx) = tokio::sync::mpsc::channel(64);
4515 self.hash_result_tx = hash_tx;
4516 self.hash_result_rx = hash_rx;
4517 if let Some(ref pool) = self.hash_pool_ref {
4520 disk_handle.set_hash_pool(pool.clone());
4521 disk_handle.set_hash_result_tx(self.hash_result_tx.clone());
4522 }
4523 self.disk = Some(disk_handle);
4524 for peer in self.peers.values() {
4527 let _ = peer
4528 .cmd_tx
4529 .try_send(PeerCommand::UpdateNumPieces(num_pieces));
4530 }
4531 let file_lengths: Vec<u64> =
4532 meta.info.files().iter().map(|f| f.length).collect();
4533 let mut meta = meta;
4534 meta.info_bytes = Some(Bytes::from(info_bytes));
4535 self.meta = Some(meta);
4536
4537 if let (Some(meta), Some(lengths)) = (&self.meta, &self.lengths) {
4539 self.cached_files = Some(build_cached_file_info(meta, lengths));
4540 }
4541
4542 self.file_priorities = vec![FilePriority::Normal; file_lengths.len()];
4543
4544 if let Some(ref selections) = self.magnet_selected_files {
4546 self.file_priorities = irontide_core::FileSelection::to_priorities(
4547 selections,
4548 file_lengths.len(),
4549 );
4550 self.magnet_selected_files = None;
4551 }
4552
4553 self.wanted_pieces = crate::piece_selector::build_wanted_pieces(
4554 &self.file_priorities,
4555 &file_lengths,
4556 self.lengths.as_ref().unwrap(),
4557 );
4558 if self.config.share_mode {
4559 self.transition_state(TorrentState::Sharing);
4560 } else {
4561 self.transition_state(TorrentState::Downloading);
4562 }
4563 self.metadata_downloader = None;
4564
4565 if let Some(ref meta) = self.meta {
4567 self.tracker_manager
4568 .set_metadata_filtered(meta, self.config.url_security);
4569 }
4570
4571 if let Ok(detected) = irontide_core::torrent_from_bytes_any(&torrent_bytes)
4574 {
4575 let new_version = detected.version();
4576 if new_version != irontide_core::TorrentVersion::V1Only {
4577 let new_hashes = detected.info_hashes();
4578 self.version = new_version;
4579 self.info_hashes = new_hashes.clone();
4580 self.tracker_manager.set_info_hashes(new_hashes.clone());
4581 if let Some(v2_meta) = detected.as_v2() {
4582 self.meta_v2 = Some(v2_meta.clone());
4583 }
4584 if new_hashes.is_hybrid()
4586 && let Some(v2) = new_hashes.v2
4587 {
4588 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
4589 if v2_as_v1 != self.info_hash {
4590 if self.dht_v2_peers_rx.is_none()
4591 && let Some(dht) = self.current_dht()
4592 && let Ok(rx) = dht.get_peers(v2_as_v1).await
4593 {
4594 self.dht_v2_peers_rx = Some(rx);
4595 }
4596 if self.dht_v6_v2_peers_rx.is_none()
4597 && self.dht_v6_empty_count < 30
4598 && self.should_retry_v6()
4599 && let Some(dht6) = self.current_dht_v6()
4600 && let Ok(rx) = dht6.get_peers(v2_as_v1).await
4601 {
4602 self.dht_v6_last_retry =
4603 Some(std::time::Instant::now());
4604 self.dht_v6_v2_peers_rx = Some(rx);
4605 }
4606 }
4607 }
4608 }
4609 }
4610
4611 let name = self
4612 .meta
4613 .as_ref()
4614 .map(|m| m.info.name.clone())
4615 .unwrap_or_default();
4616 post_alert(
4617 &self.alert_tx,
4618 &self.alert_mask,
4619 AlertKind::MetadataReceived {
4620 info_hash: self.info_hash,
4621 name,
4622 },
4623 );
4624 info!("metadata assembled, switching to Downloading");
4625
4626 if let Some(ct) = &self.chunk_tracker {
4628 let atomic_states = Arc::new(AtomicPieceStates::new(
4629 self.num_pieces,
4630 ct.bitfield(),
4631 &self.wanted_pieces,
4632 ));
4633 self.atomic_states = Some(Arc::clone(&atomic_states));
4634 self.piece_owner = vec![None; self.num_pieces as usize];
4635 self.inflight_started = vec![None; self.num_pieces as usize];
4637 self.max_in_flight = self.config.max_in_flight_pieces;
4638
4639 if self.config.use_block_stealing {
4641 if let Some(ref lengths) = self.lengths {
4642 self.block_maps =
4643 Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
4644 }
4645 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
4646 }
4647 self.piece_write_guards = Some(Arc::new(
4649 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
4650 ));
4651
4652 self.piece_tracker = Some(PieceTracker::new(
4654 self.num_pieces,
4655 ct.bitfield(),
4656 &self.wanted_pieces,
4657 ));
4658 if let Some(ref cached) = self.cached_files {
4659 let file_piece_ranges: Vec<(u32, u32)> = cached
4660 .entries
4661 .iter()
4662 .map(|e| (e.first_piece, e.last_piece))
4663 .collect();
4664 let om = Arc::new(PieceOrderMap::build(
4665 &self.file_priorities,
4666 &file_piece_ranges,
4667 self.num_pieces,
4668 0,
4669 ));
4670 self.order_map_tx.send_replace(om);
4671 }
4672
4673 let notify = Arc::new(tokio::sync::Notify::new());
4674 self.reservation_notify = Some(notify);
4675 }
4676
4677 self.spawn_web_seeds();
4679 self.assign_pieces_to_web_seeds();
4680
4681 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4684 info!(
4685 connected_peers = peer_addrs.len(),
4686 "kick-starting piece requests for pre-connected peers"
4687 );
4688 for addr in peer_addrs {
4689 let has_bitfield =
4690 self.peers.get(&addr).map_or(0, |p| p.bitfield.count_ones());
4691 let is_choking = self.peers.get(&addr).is_none_or(|p| p.peer_choking);
4692 debug!(%addr, has_bitfield, is_choking, "post-metadata peer state");
4693 self.maybe_express_interest(addr).await;
4694 if let Some(peer) = self.peers.get(&addr)
4695 && peer.bitfield.count_ones() > 0
4696 {
4697 let _slot = self.peer_slab.insert(addr);
4698 }
4699 }
4700 self.recalc_max_in_flight();
4701 if !self.user_seed_mode
4705 && let Some(notify) = &self.reservation_notify
4706 && let Some(ref lengths) = self.lengths
4707 {
4708 for peer in self.peers.values() {
4709 let _ = peer.cmd_tx.try_send(PeerCommand::StartRequesting {
4710 piece_notify: Arc::clone(notify),
4711 disk_handle: self.disk.clone(),
4712 write_error_tx: self.write_error_tx.clone(),
4713 lengths: lengths.clone(),
4714 });
4715 }
4716 }
4717 }
4718 Err(e) => {
4719 warn!("failed to parse assembled metadata: {e}");
4720 post_alert(
4721 &self.alert_tx,
4722 &self.alert_mask,
4723 AlertKind::MetadataFailed {
4724 info_hash: self.info_hash,
4725 },
4726 );
4727 }
4728 }
4729 }
4730 Err(e) => {
4731 warn!("metadata assembly failed: {e}");
4732 post_alert(
4733 &self.alert_tx,
4734 &self.alert_mask,
4735 AlertKind::MetadataFailed {
4736 info_hash: self.info_hash,
4737 },
4738 );
4739 }
4740 }
4741 }
4742
4743 fn spawn_web_seeds(&mut self) {
4746 if !self.config.enable_web_seed {
4747 return;
4748 }
4749 let Some(meta) = &self.meta else { return };
4750 let lengths = match &self.lengths {
4751 Some(l) => l.clone(),
4752 None => return,
4753 };
4754
4755 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
4756 let file_map = irontide_storage::FileMap::new(file_lengths, lengths.clone());
4757
4758 for url in &meta.url_list {
4760 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4761 continue;
4762 }
4763 if self.web_seeds.len() >= self.config.max_web_seeds {
4764 break;
4765 }
4766
4767 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4769 warn!(%url, %e, "web seed URL rejected by security policy");
4770 continue;
4771 }
4772
4773 let url_builder = if meta.info.length.is_some() {
4774 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone())
4775 } else {
4776 let file_paths: Vec<String> = meta
4777 .info
4778 .files()
4779 .iter()
4780 .map(|f| f.path[1..].join("/")) .collect();
4782 crate::web_seed::WebSeedUrlBuilder::multi(
4783 url.clone(),
4784 meta.info.name.clone(),
4785 file_paths,
4786 )
4787 };
4788
4789 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4790 let initial_downloaded = self
4791 .web_seed_stats
4792 .get(url)
4793 .map_or(0, |s| s.downloaded_bytes);
4794 let task = crate::web_seed::WebSeedTask::new(
4795 url.clone(),
4796 crate::web_seed::WebSeedMode::GetRight,
4797 url_builder,
4798 lengths.clone(),
4799 file_map.clone(),
4800 self.info_hash,
4801 cmd_rx,
4802 self.event_tx.clone(),
4803 self.config.url_security,
4804 self.config.web_seed_progress_throttle_ms,
4805 initial_downloaded,
4806 self.config.web_seed_retry_base_secs,
4807 self.config.web_seed_retry_factor,
4808 self.config.web_seed_retry_cap_secs,
4809 self.config.web_seed_max_failures,
4810 );
4811 tokio::spawn(task.run());
4812 self.web_seeds.insert(url.clone(), cmd_tx);
4813 debug!(url, "spawned BEP 19 web seed");
4814 }
4815
4816 for url in &meta.httpseeds {
4818 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4819 continue;
4820 }
4821 if self.web_seeds.len() >= self.config.max_web_seeds {
4822 break;
4823 }
4824
4825 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4827 warn!(%url, %e, "web seed URL rejected by security policy");
4828 continue;
4829 }
4830
4831 let url_builder =
4833 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone());
4834
4835 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4836 let initial_downloaded = self
4837 .web_seed_stats
4838 .get(url)
4839 .map_or(0, |s| s.downloaded_bytes);
4840 let task = crate::web_seed::WebSeedTask::new(
4841 url.clone(),
4842 crate::web_seed::WebSeedMode::Hoffman,
4843 url_builder,
4844 lengths.clone(),
4845 file_map.clone(),
4846 self.info_hash,
4847 cmd_rx,
4848 self.event_tx.clone(),
4849 self.config.url_security,
4850 self.config.web_seed_progress_throttle_ms,
4851 initial_downloaded,
4852 self.config.web_seed_retry_base_secs,
4853 self.config.web_seed_retry_factor,
4854 self.config.web_seed_retry_cap_secs,
4855 self.config.web_seed_max_failures,
4856 );
4857 tokio::spawn(task.run());
4858 self.web_seeds.insert(url.clone(), cmd_tx);
4859 debug!(url, "spawned BEP 17 web seed");
4860 }
4861 }
4862
4863 pub(crate) fn assign_pieces_to_web_seeds(&mut self) {
4864 if self.state != TorrentState::Downloading || self.end_game.is_active() {
4865 return;
4866 }
4867
4868 let active_urls: HashSet<&String> = self.web_seed_in_flight.values().collect();
4870 let idle_urls: Vec<String> = self
4871 .web_seeds
4872 .keys()
4873 .filter(|u| !active_urls.contains(u))
4874 .cloned()
4875 .collect();
4876
4877 let Some(ct) = &self.chunk_tracker else {
4878 return;
4879 };
4880
4881 for url in idle_urls {
4882 let piece = (0..self.num_pieces).find(|&i| {
4885 !ct.has_piece(i)
4886 && !self
4887 .piece_owner
4888 .get(i as usize)
4889 .is_some_and(std::option::Option::is_some)
4890 && !self.web_seed_in_flight.contains_key(&i)
4891 && self.wanted_pieces.get(i)
4892 });
4893
4894 if let Some(piece) = piece
4895 && let Some(cmd_tx) = self.web_seeds.get(&url)
4896 {
4897 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::FetchPiece(piece));
4898 self.web_seed_in_flight.insert(piece, url);
4899 }
4900 }
4901 }
4902
4903 pub(crate) async fn handle_web_seed_piece_data(
4904 &mut self,
4905 url: String,
4906 index: u32,
4907 data: Bytes,
4908 ) {
4909 self.web_seed_in_flight.remove(&index);
4910
4911 if let Some(ref ct) = self.chunk_tracker
4913 && ct.has_piece(index)
4914 {
4915 self.assign_pieces_to_web_seeds();
4916 return;
4917 }
4918
4919 if let Some(ref disk) = self.disk
4921 && let Err(e) = disk
4922 .write_chunk(index, 0, data.clone(), DiskJobFlags::FLUSH_PIECE)
4923 .await
4924 {
4925 warn!(index, "web seed: failed to write piece: {e}");
4926 self.assign_pieces_to_web_seeds();
4927 return;
4928 }
4929
4930 if let Some(ref mut ct) = self.chunk_tracker
4932 && let Some(ref lengths) = self.lengths
4933 {
4934 let num_chunks = lengths.chunks_in_piece(index);
4935 for chunk_idx in 0..num_chunks {
4936 if let Some((begin, _len)) = lengths.chunk_info(index, chunk_idx) {
4937 ct.chunk_received(index, begin);
4938 }
4939 }
4940 }
4941
4942 self.downloaded += data.len() as u64;
4943 self.total_download += data.len() as u64 + 13; self.last_download = now_unix();
4945 self.need_save_resume = true;
4946
4947 self.verify_and_mark_piece(index).await;
4949
4950 if let Some(ref ct) = self.chunk_tracker
4952 && !ct.has_piece(index)
4953 {
4954 self.ban_web_seed(&url);
4955 return;
4956 }
4957
4958 self.assign_pieces_to_web_seeds();
4959 }
4960
4961 pub(crate) fn handle_web_seed_error(&mut self, url: &str, piece: u32, message: &str) {
4962 self.web_seed_in_flight.remove(&piece);
4963 warn!(%url, piece, %message, "web seed error");
4964 self.assign_pieces_to_web_seeds();
4965 }
4966
4967 pub(crate) fn handle_web_seed_progress(
4975 &mut self,
4976 url: &str,
4977 bytes: u64,
4978 rate_bps: u64,
4979 error: Option<String>,
4980 ) {
4981 let now_unix = std::time::SystemTime::now()
4982 .duration_since(std::time::UNIX_EPOCH)
4983 .map_or(0, |d| d.as_secs());
4984 let entry = self
4985 .web_seed_stats
4986 .entry(url.to_owned())
4987 .or_insert_with(|| irontide_core::WebSeedStats {
4988 url: url.to_owned(),
4989 ..Default::default()
4990 });
4991 entry.downloaded_bytes = bytes;
4992 entry.last_rate_bps = rate_bps;
4993 entry.last_attempt_unix_secs = now_unix;
4994 if let Some(msg) = error {
4995 entry.state = irontide_core::WebSeedState::Errored;
4996 entry.last_error = Some(msg);
4997 entry.consecutive_failures = entry.consecutive_failures.saturating_add(1);
4998 let attempt = entry.consecutive_failures.saturating_sub(1);
5000 let secs = self
5001 .config
5002 .web_seed_retry_base_secs
5003 .saturating_mul(self.config.web_seed_retry_factor.saturating_pow(attempt))
5004 .min(self.config.web_seed_retry_cap_secs);
5005 entry.next_retry_unix_secs = Some(now_unix + secs);
5006 } else {
5007 entry.state = irontide_core::WebSeedState::Active;
5008 entry.consecutive_failures = 0;
5009 entry.next_retry_unix_secs = None;
5010 }
5011 self.need_save_resume = true;
5012 }
5013
5014 pub(crate) fn ban_web_seed(&mut self, url: &str) {
5015 warn!(%url, "banning web seed due to hash failure");
5016 self.banned_web_seeds.insert(url.to_owned());
5017
5018 if let Some(cmd_tx) = self.web_seeds.remove(url) {
5020 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::Shutdown);
5021 }
5022
5023 self.web_seed_in_flight.retain(|_, v| v != url);
5025
5026 post_alert(
5027 &self.alert_tx,
5028 &self.alert_mask,
5029 AlertKind::WebSeedBanned {
5030 info_hash: self.info_hash,
5031 url: url.to_owned(),
5032 },
5033 );
5034 }
5035
5036 async fn shutdown_web_seeds(&mut self) {
5037 for (_, cmd_tx) in self.web_seeds.drain() {
5038 let _ = cmd_tx.send(crate::web_seed::WebSeedCommand::Shutdown).await;
5039 }
5040 self.web_seed_in_flight.clear();
5041 }
5042
5043 fn refresh_peer_rates(&mut self) {
5045 self.cached_peer_rates.clear();
5046 self.cached_peer_rates.reserve(self.peers.len());
5047 for (&addr, p) in &self.peers {
5048 self.cached_peer_rates.insert(addr, p.pipeline.ewma_rate());
5049 }
5050 }
5051
5052 fn update_peer_rates(&mut self) {
5055 for peer in self.peers.values_mut() {
5056 peer.download_rate = peer.download_bytes_window / 2;
5057 peer.upload_rate = peer.upload_bytes_window / 2;
5058 peer.download_bytes_window = 0;
5059 peer.upload_bytes_window = 0;
5060 }
5061
5062 let aggregate_download: u64 = self.peers.values().map(|p| p.download_rate).sum();
5064 if aggregate_download > self.peak_download_rate {
5065 self.peak_download_rate = aggregate_download;
5066 }
5067 }
5068
5069 async fn run_choker(&mut self) {
5070 let peer_infos: Vec<ChokerPeerInfo> = self
5071 .peers
5072 .values()
5073 .map(|p| ChokerPeerInfo {
5074 addr: p.addr,
5075 download_rate: p.download_rate,
5076 upload_rate: p.upload_rate,
5077 interested: p.peer_interested,
5078 upload_only: p.upload_only,
5079 is_seed: p.upload_only
5080 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
5081 })
5082 .collect();
5083
5084 let decision = self.choker.decide(&peer_infos);
5085
5086 for addr in &decision.to_unchoke {
5087 if let Some(peer) = self.peers.get_mut(addr)
5088 && peer.am_choking
5089 {
5090 peer.am_choking = false;
5091 if peer.am_unchoke_started_at.is_none() {
5093 peer.am_unchoke_started_at = Some(Instant::now());
5094 }
5095 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(false));
5096 }
5097 }
5098
5099 for addr in &decision.to_choke {
5100 if let Some(peer) = self.peers.get_mut(addr)
5101 && !peer.am_choking
5102 {
5103 if peer.supports_fast {
5104 let pending: Vec<(u32, u32, u32)> = peer.incoming_requests.drain(..).collect();
5105 for (index, begin, length) in pending {
5106 let _ = peer.cmd_tx.try_send(PeerCommand::RejectRequest {
5107 index,
5108 begin,
5109 length,
5110 });
5111 }
5112 }
5113 peer.am_choking = true;
5114 if let Some(start) = peer.am_unchoke_started_at.take() {
5116 peer.unchoke_duration_total += start.elapsed();
5117 }
5118 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(true));
5119 }
5120 }
5121
5122 self.serve_incoming_requests().await;
5124
5125 if self.state == TorrentState::Downloading {
5130 let zombie_threshold = Duration::from_secs(30);
5131 let zombies: Vec<SocketAddr> = self
5132 .peers
5133 .values()
5134 .filter(|p| {
5135 p.bitfield.count_ones() == 0 && p.connected_at.elapsed() > zombie_threshold
5136 })
5137 .map(|p| p.addr)
5138 .collect();
5139
5140 for &addr in &zombies {
5141 debug!(%addr, "disconnecting zombie peer (empty bitfield after 30s)");
5142 self.disconnect_peer(addr, "zombie peer (empty bitfield)");
5143 }
5144 if !zombies.is_empty() {
5145 self.recalc_max_in_flight();
5146 }
5147 }
5148 }
5149
5150 fn rotate_optimistic(&mut self) {
5151 let peer_infos: Vec<ChokerPeerInfo> = self
5152 .peers
5153 .values()
5154 .map(|p| ChokerPeerInfo {
5155 addr: p.addr,
5156 download_rate: p.download_rate,
5157 upload_rate: p.upload_rate,
5158 interested: p.peer_interested,
5159 upload_only: p.upload_only,
5160 is_seed: p.upload_only
5161 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
5162 })
5163 .collect();
5164
5165 self.choker.rotate_optimistic(&peer_infos);
5166 }
5167
5168 fn handle_i2p_incoming(&mut self, stream: crate::i2p::SamStream) {
5174 if self.peers.len() >= self.effective_max_connections() {
5175 return;
5176 }
5177
5178 let synthetic_addr = self.next_i2p_synthetic_addr();
5179
5180 let remote_dest = stream.remote_destination().clone();
5181 let dest_preview = {
5182 let b64 = remote_dest.to_base64();
5183 if b64.len() >= 8 {
5184 b64[..8].to_string()
5185 } else {
5186 b64
5187 }
5188 };
5189 self.i2p_destinations.insert(synthetic_addr, remote_dest);
5190 let tcp_stream = stream.into_inner();
5191
5192 self.spawn_peer_from_stream(synthetic_addr, tcp_stream);
5193
5194 debug!(dest = %dest_preview, addr = %synthetic_addr, "accepted I2P peer");
5195 }
5196
5197 #[allow(dead_code)] fn add_i2p_peer(
5200 &mut self,
5201 dest: crate::i2p::I2pDestination,
5202 source: PeerSource,
5203 ) -> Option<SocketAddr> {
5204 if self.i2p_destinations.values().any(|d| d == &dest) {
5206 return None;
5207 }
5208 let addr = self.next_i2p_synthetic_addr();
5209 self.i2p_destinations.insert(addr, dest);
5210 if let Some(ref ps) = self.peer_states {
5212 ps.add_if_not_seen(addr, source);
5213 }
5214 Some(addr)
5215 }
5216
5217 fn next_i2p_synthetic_addr(&mut self) -> SocketAddr {
5223 self.i2p_peer_counter = self.i2p_peer_counter.wrapping_add(1);
5224 let a = ((self.i2p_peer_counter >> 16) & 0x0F) as u8 | 0xF0;
5225 let b = ((self.i2p_peer_counter >> 8) & 0xFF) as u8;
5226 let c = (self.i2p_peer_counter & 0xFF) as u8;
5227 SocketAddr::new(
5228 std::net::IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, 1)),
5229 (self.i2p_peer_counter & 0xFFFF) as u16,
5230 )
5231 }
5232}
5233
5234pub(crate) fn is_i2p_synthetic_addr(addr: &SocketAddr) -> bool {
5236 match addr {
5237 SocketAddr::V4(v4) => v4.ip().octets()[0] & 0xF0 == 0xF0,
5238 SocketAddr::V6(_) => false,
5239 }
5240}
5241
5242async fn accept_incoming(
5245 listener: &mut Option<Box<dyn crate::transport::TransportListener>>,
5246) -> std::io::Result<(crate::transport::BoxedStream, SocketAddr)> {
5247 match listener {
5248 Some(l) => l.accept().await,
5249 None => std::future::pending().await,
5250 }
5251}
5252
5253async fn accept_i2p(
5256 rx: &mut Option<mpsc::Receiver<crate::i2p::SamStream>>,
5257) -> Option<crate::i2p::SamStream> {
5258 match rx {
5259 Some(rx) => rx.recv().await,
5260 None => std::future::pending().await,
5261 }
5262}
5263
5264pub(crate) fn serve_hashes(
5275 meta_v2: Option<&irontide_core::TorrentMetaV2>,
5276 version: irontide_core::TorrentVersion,
5277 lengths: Option<&Lengths>,
5278 request: &irontide_core::HashRequest,
5279) -> Option<Vec<irontide_core::Id32>> {
5280 let meta_v2 = match meta_v2 {
5282 Some(m) if version != irontide_core::TorrentVersion::V1Only => m,
5283 _ => return None,
5284 };
5285
5286 let piece_hashes = meta_v2.file_piece_hashes(&request.file_root)?;
5288
5289 let lengths = lengths?;
5291
5292 let blocks_per_piece = (meta_v2.info.piece_length / u64::from(lengths.chunk_size())) as u32;
5297 let num_pieces = piece_hashes.len() as u32;
5298 let num_blocks = num_pieces.saturating_mul(blocks_per_piece);
5299
5300 if !irontide_core::validate_hash_request(request, num_blocks, num_pieces) {
5301 return None;
5302 }
5303
5304 let piece_layer_base = blocks_per_piece.trailing_zeros();
5307 if request.base != piece_layer_base {
5308 return None;
5309 }
5310
5311 let start = request.index as usize;
5313 let end = (start + request.count as usize).min(piece_hashes.len());
5314 let mut hashes: Vec<irontide_core::Id32> = piece_hashes[start..end].to_vec();
5315
5316 if request.proof_layers > 0 && !piece_hashes.is_empty() {
5324 let tree = irontide_core::MerkleTree::from_leaves(&piece_hashes);
5325 let full_proof = tree.proof_path(start);
5326 let subtree_depth = if request.count > 1 {
5328 (request.count as usize)
5329 .next_power_of_two()
5330 .trailing_zeros() as usize
5331 } else {
5332 0
5333 };
5334 let available = full_proof.len().saturating_sub(subtree_depth);
5335 let proof_count = (request.proof_layers as usize).min(available);
5336 hashes.extend_from_slice(&full_proof[subtree_depth..subtree_depth + proof_count]);
5337 }
5338
5339 Some(hashes)
5340}
5341
5342#[cfg(test)]
5347impl TorrentActor {
5348 pub(crate) fn for_throttle_test(num_pieces: u32, _throttle_ms: u64) -> Self {
5363 use irontide_storage::Bitfield;
5364
5365 let config = TorrentConfig {
5366 ..TorrentConfig::default()
5367 };
5368
5369 let info_hash = Id20([0u8; 20]);
5370 let our_peer_id = Id20([0u8; 20]);
5371
5372 let (_cmd_tx, cmd_rx) = mpsc::channel(1);
5373 let (event_tx, event_rx) = mpsc::channel(1);
5374 let (write_error_tx, write_error_rx) = mpsc::channel(1);
5375 let (verify_result_tx, verify_result_rx) = mpsc::channel(1);
5376 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
5377 let (piece_ready_tx, _piece_ready_rx) = broadcast::channel(1);
5378 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(num_pieces));
5379 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
5380 let (alert_tx, _alert_rx) = broadcast::channel(64);
5381 let (_disk_mgr_tx, _disk_mgr_rx) = mpsc::channel::<crate::disk::DiskJob>(1);
5382
5383 let stream_read_semaphore = Arc::new(tokio::sync::Semaphore::new(8));
5384 let alert_mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
5385
5386 let (disk_manager, _disk_join) =
5388 crate::disk::DiskManagerHandle::new(crate::disk::DiskConfig::default());
5389
5390 let ban_manager = Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
5391 crate::ban::BanConfig::default(),
5392 )));
5393 let ip_filter = Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
5394
5395 let upload_bucket = crate::rate_limiter::TokenBucket::new(0);
5396 let download_bucket = Arc::new(parking_lot::Mutex::new(
5397 crate::rate_limiter::TokenBucket::new(0),
5398 ));
5399 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(0, 0, 0, 0, 0, 0);
5400
5401 let dht_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5402 let dht_v6_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5403 let factory = Arc::new(crate::transport::NetworkFactory::tokio());
5404
5405 let we_have = Bitfield::new(num_pieces);
5409 let mut wanted = Bitfield::new(num_pieces);
5410 for i in 0..num_pieces {
5411 wanted.set(i);
5412 }
5413 let atomic_states = Arc::new(crate::piece_reservation::AtomicPieceStates::new(
5414 num_pieces, &we_have, &wanted,
5415 ));
5416
5417 let (order_map_tx, _order_map_rx_seed) =
5418 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
5419
5420 Self {
5421 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(0),
5422 config,
5423 info_hash,
5424 our_peer_id,
5425 state: TorrentState::Downloading,
5426 disk: None,
5427 disk_manager,
5428 chunk_tracker: None,
5429 lengths: None,
5430 num_pieces,
5431 file_priorities: Vec::new(),
5432 wanted_pieces: Bitfield::new(num_pieces),
5433 end_game: EndGame::new(),
5434 streaming_pieces: BTreeSet::new(),
5435 time_critical_pieces: BTreeSet::new(),
5436 streaming_cursors: Vec::new(),
5437 piece_ready_tx,
5438 have_watch_tx,
5439 have_watch_rx,
5440 stream_read_semaphore,
5441 peers: HashMap::new(),
5442 unchoke_durations: HashMap::new(),
5443 cached_peer_rates: FxHashMap::default(),
5444 refill_notify: Arc::new(tokio::sync::Notify::new()),
5445 atomic_states: Some(atomic_states),
5446 block_maps: None,
5447 steal_candidates: None,
5448 last_steal_populate: Instant::now(),
5449 piece_write_guards: None,
5450 soft_reap_buf: Vec::new(),
5451 eviction_history: std::collections::VecDeque::new(),
5452 force_immediate_choker_tick: false,
5453 piece_tracker: None,
5454 order_map_dirty: false,
5455 next_order_map_gen: 0,
5456 order_map_tx,
5457 piece_owner: vec![None; num_pieces as usize],
5458 peer_slab: crate::piece_reservation::PeerSlab::new(),
5459 priority_pieces: BTreeSet::new(),
5460 max_in_flight: 512,
5461 reservation_notify: None,
5462 last_tick_dispatch_state: None,
5463 choker: Choker::new(4),
5464 user_seed_mode: false,
5465 user_forced: false,
5466 max_connections: 0,
5467 peer_states: None,
5468 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
5469 connect_permits: HashMap::new(),
5470 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
5471 std::collections::HashMap::new(),
5472 )),
5473 connect_rx: None,
5474 metadata_downloader: None,
5475 meta: None,
5476 cached_files: None,
5477 downloaded: 0,
5478 uploaded: 0,
5479 checking_progress: 0.0,
5480 total_download: 0,
5481 total_upload: 0,
5482 total_failed_bytes: 0,
5483 total_redundant_bytes: 0,
5484 added_time: 0,
5485 completed_time: 0,
5486 last_download: 0,
5487 last_upload: 0,
5488 last_seen_complete: 0,
5489 active_duration: 0,
5490 finished_duration: 0,
5491 seeding_duration: 0,
5492 active_since: None,
5493 state_duration_since: None,
5494 started_at: Instant::now(),
5495 moving_storage: false,
5496 has_incoming: false,
5497 need_save_resume: false,
5498 error: String::new(),
5499 error_file: -1,
5500 cmd_rx,
5501 event_tx,
5502 event_rx,
5503 write_error_rx,
5504 write_error_tx,
5505 verify_result_rx,
5506 verify_result_tx,
5507 pending_verify: HashSet::new(),
5508 piece_generations: vec![0u64; num_pieces as usize],
5509 hash_result_rx,
5510 hash_result_tx,
5511 listener: None,
5512 utp_socket: None,
5513 utp_socket_v6: None,
5514 tracker_manager: TrackerManager::empty(info_hash, our_peer_id, 0, 0, false),
5515 tracker_result_rx: None,
5516 dht_rx,
5517 dht_v6_rx,
5518 dht_enabled: false,
5519 dht_peers_rx: None,
5520 dht_v6_peers_rx: None,
5521 dht_v6_empty_count: 0,
5522 dht_v6_last_retry: None,
5523 alert_tx,
5524 alert_mask,
5525 upload_bucket,
5526 download_bucket,
5527 global_upload_bucket: None,
5528 global_download_bucket: None,
5529 slot_tuner: crate::slot_tuner::SlotTuner::disabled(4),
5530 upload_bytes_interval: 0,
5531 peak_download_rate: 0,
5532 web_seeds: HashMap::new(),
5533 banned_web_seeds: HashSet::new(),
5534 web_seed_in_flight: HashMap::new(),
5535 web_seed_stats: HashMap::new(),
5536 pex_peer_count: 0,
5537 lsd_peer_count: 0,
5538 super_seed: None,
5539 have_broadcast_tx,
5540 suggested_to_peers: HashMap::new(),
5541 predictive_have_sent: HashSet::new(),
5542 ban_manager,
5543 piece_contributors: HashMap::new(),
5544 parole_pieces: HashMap::new(),
5545 ip_filter,
5546 external_ip: None,
5547 share_lru: std::collections::VecDeque::new(),
5548 share_max_pieces: 0,
5549 plugins: Arc::new(Vec::new()),
5550 hash_picker: None,
5551 version: irontide_core::TorrentVersion::V1Only,
5552 meta_v2: None,
5553 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
5554 dht_v2_peers_rx: None,
5555 dht_v6_v2_peers_rx: None,
5556 magnet_selected_files: None,
5557 sam_session: None,
5558 i2p_accept_rx: None,
5559 i2p_peer_counter: 0,
5560 i2p_destinations: HashMap::new(),
5561 ssl_manager: None,
5562 rate_limiter_set,
5563 auto_sequential_active: false,
5564 factory,
5565 hash_pool_ref: None,
5566 connect_attempts: 0,
5567 connect_failures: 0,
5568 choke_rotations: 0,
5569 inflight_started: Vec::new(),
5570 completed_piece_times: std::collections::VecDeque::new(),
5571 piece_steals: 0,
5572 holepunch_relayed: 0,
5573 holepunch_relay_rate: HashMap::new(),
5574 holepunch_cooldowns: HashMap::new(),
5575 holepunch_pending: Vec::new(),
5576 counters: Arc::new(crate::stats::SessionCounters::new()),
5577 }
5578 }
5579}
5580
5581#[cfg(test)]
5586mod tests {
5587 use super::*;
5588 use bytes::Bytes;
5589 use futures::{SinkExt, StreamExt};
5590 use irontide_wire::{ExtHandshake, Handshake, Message, MessageCodec};
5591 use std::time::Duration;
5592 use tokio::io::{AsyncReadExt, AsyncWriteExt};
5593 use tokio::net::TcpListener;
5594 use tokio_util::codec::{FramedRead, FramedWrite};
5595
5596 #[test]
5599 fn initial_unchoke_slots_unlimited_returns_default_four() {
5600 assert_eq!(initial_unchoke_slots(-1), 4);
5601 }
5602
5603 #[test]
5604 fn initial_unchoke_slots_capped_returns_value() {
5605 assert_eq!(initial_unchoke_slots(1), 1);
5606 assert_eq!(initial_unchoke_slots(4), 4);
5607 assert_eq!(initial_unchoke_slots(16), 16);
5608 }
5609
5610 fn make_test_torrent(data: &[u8], piece_length: u64) -> TorrentMetaV1 {
5614 use serde::Serialize;
5615
5616 #[derive(Serialize)]
5617 struct Info<'a> {
5618 length: u64,
5619 name: &'a str,
5620 #[serde(rename = "piece length")]
5621 piece_length: u64,
5622 #[serde(with = "serde_bytes")]
5623 pieces: &'a [u8],
5624 }
5625
5626 #[derive(Serialize)]
5627 struct Torrent<'a> {
5628 info: Info<'a>,
5629 }
5630
5631 let mut pieces = Vec::new();
5632 let mut offset = 0;
5633 while offset < data.len() {
5634 let end = (offset + piece_length as usize).min(data.len());
5635 let hash = irontide_core::sha1(&data[offset..end]);
5636 pieces.extend_from_slice(hash.as_bytes());
5637 offset = end;
5638 }
5639
5640 let t = Torrent {
5641 info: Info {
5642 length: data.len() as u64,
5643 name: "test",
5644 piece_length,
5645 pieces: &pieces,
5646 },
5647 };
5648
5649 let bytes = irontide_bencode::to_bytes(&t).unwrap();
5650 torrent_from_bytes(&bytes).unwrap()
5651 }
5652
5653 fn test_config() -> TorrentConfig {
5654 TorrentConfig {
5655 listen_port: 0, max_peers: 200,
5657 target_request_queue: 5,
5658 download_dir: std::path::PathBuf::from("/tmp"),
5659 enable_dht: false,
5660 enable_pex: false,
5661 enable_fast: false,
5662 seed_ratio_limit: None,
5663 seed_time_limit_secs: None,
5664 inactive_seed_time_limit_secs: None,
5665 strict_end_game: true,
5666 upload_rate_limit: 0,
5667 download_rate_limit: 0,
5668 max_uploads_per_torrent: -1,
5669 encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
5670 enable_utp: false,
5671 enable_web_seed: true,
5672 enable_holepunch: false,
5673 enable_bep40_eviction: true,
5674 max_web_seeds: 4,
5675 web_seed_retry_base_secs: 10,
5676 web_seed_retry_factor: 6,
5677 web_seed_retry_cap_secs: 3600,
5678 web_seed_max_failures: 10,
5679 super_seeding: false,
5680 upload_only_announce: true,
5681 hashing_threads: 2,
5682 sequential_download: false,
5683 initial_picker_threshold: 4,
5684 whole_pieces_threshold: 20,
5685 snub_timeout_secs: 15,
5686 readahead_pieces: 8,
5687 streaming_timeout_escalation: true,
5688 max_concurrent_stream_reads: 8,
5689 proxy: crate::proxy::ProxyConfig::default(),
5690 anonymous_mode: false,
5691 share_mode: false,
5692 enable_i2p: false,
5693 allow_i2p_mixed: false,
5694 ssl_listen_port: 0,
5695 seed_choking_algorithm: crate::choker::SeedChokingAlgorithm::FastestUpload,
5696 choking_algorithm: crate::choker::ChokingAlgorithm::FixedSlots,
5697 piece_extent_affinity: true,
5698 suggest_mode: false,
5699 max_suggest_pieces: 10,
5700 predictive_piece_announce_ms: 0,
5701 mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
5702 auto_sequential: true,
5703 storage_mode: irontide_core::StorageMode::Auto,
5704 preallocate_mode: None,
5705 block_request_timeout_secs: 60,
5706 enable_lsd: false,
5707 force_proxy: false,
5708 steal_threshold_ratio: 10.0,
5709 steal_threshold_endgame: 3.0,
5710 peer_read_timeout_secs: 0, peer_write_timeout_secs: 0, data_contribution_timeout_secs: 0, pass0_grace_secs: 60,
5715 proactive_evictions_per_minute_limit: 30,
5716 eviction_ban_duration_secs: 600,
5717 eviction_ban_set_cap: 1024,
5718 choke_rotation_max_evictions: 0, max_concurrent_connects: 128,
5720 connect_soft_timeout: 3,
5721 dispatch_backlog_cap: 8,
5722 event_backlog_cap: 32,
5723 peer_writer_channel_cap: 1024,
5724 use_actor_dispatch: true,
5725 web_seed_progress_throttle_ms: 250,
5726 url_security: crate::url_guard::UrlSecurityConfig::default(),
5727 peer_connect_timeout: 2,
5728 peer_dscp: 0x08,
5729 initial_queue_depth: 128,
5730 max_request_queue_depth: 250,
5731 request_queue_time: 3.0,
5732 max_metadata_size: 4 * 1024 * 1024,
5733 max_message_size: 16 * 1024 * 1024,
5734 max_piece_length: 32 * 1024 * 1024,
5735 max_outstanding_requests: 500,
5736 max_in_flight_pieces: 20,
5737 use_block_stealing: true,
5738 steal_stale_piece_secs: 2,
5739 fixed_pipeline_depth: 128,
5740 lock_warn_threshold_ms: 0, filesystem_direct_io: false,
5742 category: None,
5743 tags: Vec::new(),
5744 }
5745 }
5746
5747 fn make_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
5748 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
5749 Arc::new(MemoryStorage::new(lengths))
5750 }
5751
5752 fn make_seeded_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
5753 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
5754 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
5755 let num_pieces = lengths.num_pieces();
5757 for p in 0..num_pieces {
5758 let piece_size = lengths.piece_size(p) as usize;
5759 let offset = lengths.piece_offset(p) as usize;
5760 let end = offset + piece_size;
5761 storage.write_chunk(p, 0, &data[offset..end]).unwrap();
5762 }
5763 storage
5764 }
5765
5766 fn test_alert_channel() -> (broadcast::Sender<Alert>, Arc<AtomicU32>) {
5767 let (tx, _) = broadcast::channel(64);
5768 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
5769 (tx, mask)
5770 }
5771
5772 fn test_ban_manager() -> irontide_session_types::SharedBanManager {
5773 Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
5774 crate::ban::BanConfig::default(),
5775 )))
5776 }
5777
5778 fn test_ip_filter() -> irontide_session_types::SharedIpFilter {
5779 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()))
5780 }
5781
5782 fn test_disk_manager() -> (DiskManagerHandle, tokio::task::JoinHandle<()>) {
5783 DiskManagerHandle::new(crate::disk::DiskConfig::default())
5784 }
5785
5786 async fn test_register_disk(
5787 info_hash: Id20,
5788 storage: Arc<dyn TorrentStorage>,
5789 ) -> (DiskHandle, DiskManagerHandle, tokio::task::JoinHandle<()>) {
5790 let (dm, join) = test_disk_manager();
5791 let dh = dm.register_torrent(info_hash, storage).await;
5792 (dh, dm, join)
5793 }
5794
5795 fn test_dht_rx() -> irontide_dht::DhtReceiver {
5798 let bx = irontide_dht::DhtBroadcast::new(None);
5801 bx.subscribe()
5802 }
5803
5804 const HANDSHAKE_SIZE: usize = 68;
5806
5807 #[tokio::test]
5810 async fn create_from_torrent() {
5811 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
5814 let config = test_config();
5815
5816 let (atx, amask) = test_alert_channel();
5817 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5818 let handle = TorrentHandle::from_torrent(
5819 meta,
5820 irontide_core::TorrentVersion::V1Only,
5821 None,
5822 dh,
5823 dm,
5824 config,
5825 test_dht_rx(),
5826 test_dht_rx(),
5827 None,
5828 None,
5829 crate::slot_tuner::SlotTuner::disabled(4),
5830 atx,
5831 amask,
5832 None,
5833 None,
5834 test_ban_manager(),
5835 test_ip_filter(),
5836 Arc::new(Vec::new()),
5837 None,
5838 None,
5839 Arc::new(crate::transport::NetworkFactory::tokio()),
5840 None, Arc::new(crate::stats::SessionCounters::new()),
5842 )
5843 .await
5844 .unwrap();
5845
5846 let stats = handle.stats().await.unwrap();
5847 assert_eq!(stats.state, TorrentState::Downloading);
5848 assert_eq!(stats.pieces_total, 2);
5849 assert_eq!(stats.pieces_have, 0);
5850 assert_eq!(stats.peers_connected, 0);
5851
5852 handle.shutdown().await.unwrap();
5853 }
5854
5855 async fn started_test_handle() -> (TorrentHandle, Vec<String>, tokio::task::JoinHandle<()>) {
5864 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
5866 let expected_hex: Vec<String> =
5867 meta.info.pieces.chunks_exact(20).map(hex::encode).collect();
5868 let storage = make_storage(&data, 16384);
5869 let config = test_config();
5870
5871 let (atx, amask) = test_alert_channel();
5872 let (dh, dm, dj) = test_register_disk(meta.info_hash, storage).await;
5873 let handle = TorrentHandle::from_torrent(
5874 meta,
5875 irontide_core::TorrentVersion::V1Only,
5876 None,
5877 dh,
5878 dm,
5879 config,
5880 test_dht_rx(),
5881 test_dht_rx(),
5882 None,
5883 None,
5884 crate::slot_tuner::SlotTuner::disabled(4),
5885 atx,
5886 amask,
5887 None,
5888 None,
5889 test_ban_manager(),
5890 test_ip_filter(),
5891 Arc::new(Vec::new()),
5892 None,
5893 None,
5894 Arc::new(crate::transport::NetworkFactory::tokio()),
5895 None,
5896 Arc::new(crate::stats::SessionCounters::new()),
5897 )
5898 .await
5899 .unwrap();
5900 (handle, expected_hex, dj)
5901 }
5902
5903 #[tokio::test]
5910 async fn take_resume_if_dirty_is_atomic_capture_and_clear() {
5911 let (handle, _expected_hex, _dj) = started_test_handle().await;
5912
5913 handle.set_tags(vec!["m245".to_string()]).await.unwrap();
5917
5918 let first = handle.take_resume_if_dirty().await.unwrap();
5919 assert!(first.is_some(), "dirty torrent must yield resume data");
5920
5921 let second = handle.take_resume_if_dirty().await.unwrap();
5922 assert!(
5923 second.is_none(),
5924 "flag was cleared atomically in the same take — no second capture"
5925 );
5926
5927 handle.shutdown().await.unwrap();
5928 }
5929
5930 #[tokio::test]
5936 async fn mark_resume_dirty_restores_capture_after_write_failure() {
5937 let (handle, _expected_hex, _dj) = started_test_handle().await;
5938
5939 handle.set_tags(vec!["m245".to_string()]).await.unwrap();
5940
5941 let captured = handle.take_resume_if_dirty().await.unwrap();
5942 assert!(captured.is_some(), "dirty torrent captured once");
5943
5944 let between = handle.take_resume_if_dirty().await.unwrap();
5946 assert!(between.is_none(), "take cleared the flag");
5947
5948 handle.mark_resume_dirty().await.unwrap();
5950
5951 let recaptured = handle.take_resume_if_dirty().await.unwrap();
5952 assert!(
5953 recaptured.is_some(),
5954 "re-dirtied torrent must re-capture — no lost resume update"
5955 );
5956
5957 handle.shutdown().await.unwrap();
5958 }
5959
5960 #[tokio::test]
5967 async fn get_piece_hashes_hex_parity_and_windowing() {
5968 let (handle, expected_hex, _dj) = started_test_handle().await;
5969 assert_eq!(expected_hex.len(), 2, "2-piece test torrent");
5970
5971 let all = handle.get_piece_hashes(0, 1000).await.unwrap();
5973 assert_eq!(
5974 all, expected_hex,
5975 "hex output must match the raw piece hashes"
5976 );
5977
5978 let windowed = handle.get_piece_hashes(1, 1).await.unwrap();
5980 assert_eq!(windowed, vec![expected_hex[1].clone()]);
5981
5982 let first = handle.get_piece_hashes(0, 1).await.unwrap();
5984 assert_eq!(first, vec![expected_hex[0].clone()]);
5985
5986 let past = handle.get_piece_hashes(99, 5).await.unwrap();
5988 assert!(past.is_empty(), "offset past end yields empty");
5989
5990 let clamped = handle.get_piece_hashes(1, 1000).await.unwrap();
5992 assert_eq!(clamped, vec![expected_hex[1].clone()]);
5993
5994 handle.shutdown().await.unwrap();
5995 }
5996
5997 #[tokio::test]
6000 async fn create_from_magnet() {
6001 let magnet = Magnet {
6002 info_hashes: irontide_core::InfoHashes::v1_only(
6003 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
6004 ),
6005 display_name: Some("test".into()),
6006 trackers: vec![],
6007 peers: vec![],
6008 selected_files: None,
6009 };
6010 let config = test_config();
6011
6012 let (atx, amask) = test_alert_channel();
6013 let (dm, _dj) = test_disk_manager();
6014 let handle = TorrentHandle::from_magnet(
6015 magnet,
6016 dm,
6017 config,
6018 test_dht_rx(),
6019 test_dht_rx(),
6020 None,
6021 None,
6022 crate::slot_tuner::SlotTuner::disabled(4),
6023 atx,
6024 amask,
6025 None,
6026 None,
6027 test_ban_manager(),
6028 test_ip_filter(),
6029 Arc::new(Vec::new()),
6030 None,
6031 None,
6032 Arc::new(crate::transport::NetworkFactory::tokio()),
6033 None, Arc::new(crate::stats::SessionCounters::new()),
6035 )
6036 .await
6037 .unwrap();
6038
6039 let stats = handle.stats().await.unwrap();
6040 assert_eq!(stats.state, TorrentState::FetchingMetadata);
6041 assert_eq!(stats.pieces_total, 0);
6042
6043 handle.shutdown().await.unwrap();
6044 }
6045
6046 #[tokio::test]
6049 async fn add_peers_increases_available() {
6050 let data = vec![0xAB; 32768];
6051 let meta = make_test_torrent(&data, 16384);
6052 let storage = make_storage(&data, 16384);
6053 let config = test_config();
6054
6055 let (atx, amask) = test_alert_channel();
6056 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6057 let handle = TorrentHandle::from_torrent(
6058 meta,
6059 irontide_core::TorrentVersion::V1Only,
6060 None,
6061 dh,
6062 dm,
6063 config,
6064 test_dht_rx(),
6065 test_dht_rx(),
6066 None,
6067 None,
6068 crate::slot_tuner::SlotTuner::disabled(4),
6069 atx,
6070 amask,
6071 None,
6072 None,
6073 test_ban_manager(),
6074 test_ip_filter(),
6075 Arc::new(Vec::new()),
6076 None,
6077 None,
6078 Arc::new(crate::transport::NetworkFactory::tokio()),
6079 None, Arc::new(crate::stats::SessionCounters::new()),
6081 )
6082 .await
6083 .unwrap();
6084
6085 let listener1 = TcpListener::bind("127.0.0.1:0").await.unwrap();
6087 let addr1 = listener1.local_addr().unwrap();
6088 let listener2 = TcpListener::bind("127.0.0.1:0").await.unwrap();
6089 let addr2 = listener2.local_addr().unwrap();
6090
6091 handle
6092 .add_peers(vec![addr1, addr2], PeerSource::Tracker)
6093 .await
6094 .unwrap();
6095
6096 tokio::time::sleep(Duration::from_millis(100)).await;
6098
6099 let stats = handle.stats().await.unwrap();
6100 assert!(
6102 stats.peers_available + stats.peers_connected >= 2,
6103 "expected at least 2 peers known, got available={}, connected={}",
6104 stats.peers_available,
6105 stats.peers_connected
6106 );
6107
6108 handle.shutdown().await.unwrap();
6109 }
6110
6111 #[tokio::test]
6114 async fn stats_reporting() {
6115 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
6118 let config = test_config();
6119
6120 let (atx, amask) = test_alert_channel();
6121 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6122 let handle = TorrentHandle::from_torrent(
6123 meta,
6124 irontide_core::TorrentVersion::V1Only,
6125 None,
6126 dh,
6127 dm,
6128 config,
6129 test_dht_rx(),
6130 test_dht_rx(),
6131 None,
6132 None,
6133 crate::slot_tuner::SlotTuner::disabled(4),
6134 atx,
6135 amask,
6136 None,
6137 None,
6138 test_ban_manager(),
6139 test_ip_filter(),
6140 Arc::new(Vec::new()),
6141 None,
6142 None,
6143 Arc::new(crate::transport::NetworkFactory::tokio()),
6144 None, Arc::new(crate::stats::SessionCounters::new()),
6146 )
6147 .await
6148 .unwrap();
6149
6150 let stats = handle.stats().await.unwrap();
6151 assert_eq!(stats.state, TorrentState::Downloading);
6152 assert_eq!(stats.downloaded, 0);
6153 assert_eq!(stats.uploaded, 0);
6154 assert_eq!(stats.pieces_have, 0);
6155 assert_eq!(stats.pieces_total, 4);
6156 assert_eq!(stats.peers_connected, 0);
6157 assert_eq!(stats.peers_available, 0);
6158
6159 handle.shutdown().await.unwrap();
6160 }
6161
6162 #[tokio::test]
6165 async fn private_torrent_disables_dht_pex() {
6166 use serde::Serialize;
6168
6169 #[derive(Serialize)]
6170 struct Info<'a> {
6171 length: u64,
6172 name: &'a str,
6173 #[serde(rename = "piece length")]
6174 piece_length: u64,
6175 #[serde(with = "serde_bytes")]
6176 pieces: &'a [u8],
6177 private: i64,
6178 }
6179
6180 #[derive(Serialize)]
6181 struct Torrent<'a> {
6182 info: Info<'a>,
6183 }
6184
6185 let data = vec![0xAB; 16384];
6186 let hash = irontide_core::sha1(&data);
6187 let mut pieces = Vec::new();
6188 pieces.extend_from_slice(hash.as_bytes());
6189
6190 let t = Torrent {
6191 info: Info {
6192 length: data.len() as u64,
6193 name: "private_test",
6194 piece_length: 16384,
6195 pieces: &pieces,
6196 private: 1,
6197 },
6198 };
6199
6200 let bytes = irontide_bencode::to_bytes(&t).unwrap();
6201 let meta = torrent_from_bytes(&bytes).unwrap();
6202 assert_eq!(meta.info.private, Some(1));
6203
6204 let storage = make_storage(&data, 16384);
6205 let mut config = test_config();
6206 config.enable_dht = true;
6207 config.enable_pex = true;
6208
6209 let (atx, amask) = test_alert_channel();
6211 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6212 let handle = TorrentHandle::from_torrent(
6213 meta,
6214 irontide_core::TorrentVersion::V1Only,
6215 None,
6216 dh,
6217 dm,
6218 config,
6219 test_dht_rx(),
6220 test_dht_rx(),
6221 None,
6222 None,
6223 crate::slot_tuner::SlotTuner::disabled(4),
6224 atx,
6225 amask,
6226 None,
6227 None,
6228 test_ban_manager(),
6229 test_ip_filter(),
6230 Arc::new(Vec::new()),
6231 None,
6232 None,
6233 Arc::new(crate::transport::NetworkFactory::tokio()),
6234 None, Arc::new(crate::stats::SessionCounters::new()),
6236 )
6237 .await
6238 .unwrap();
6239
6240 let stats = handle.stats().await.unwrap();
6244 assert_eq!(stats.state, TorrentState::Downloading);
6245
6246 handle.shutdown().await.unwrap();
6247 }
6248
6249 #[tokio::test]
6252 async fn shutdown_cleanup() {
6253 let data = vec![0xAB; 16384];
6254 let meta = make_test_torrent(&data, 16384);
6255 let storage = make_storage(&data, 16384);
6256 let config = test_config();
6257
6258 let (atx, amask) = test_alert_channel();
6259 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6260 let handle = TorrentHandle::from_torrent(
6261 meta,
6262 irontide_core::TorrentVersion::V1Only,
6263 None,
6264 dh,
6265 dm,
6266 config,
6267 test_dht_rx(),
6268 test_dht_rx(),
6269 None,
6270 None,
6271 crate::slot_tuner::SlotTuner::disabled(4),
6272 atx,
6273 amask,
6274 None,
6275 None,
6276 test_ban_manager(),
6277 test_ip_filter(),
6278 Arc::new(Vec::new()),
6279 None,
6280 None,
6281 Arc::new(crate::transport::NetworkFactory::tokio()),
6282 None, Arc::new(crate::stats::SessionCounters::new()),
6284 )
6285 .await
6286 .unwrap();
6287
6288 handle.shutdown().await.unwrap();
6289
6290 tokio::time::sleep(Duration::from_millis(50)).await;
6292 let result = handle.stats().await;
6293 assert!(result.is_err());
6294 }
6295
6296 #[tokio::test]
6299 async fn duplicate_peers_ignored() {
6300 let data = vec![0xAB; 16384];
6301 let meta = make_test_torrent(&data, 16384);
6302 let storage = make_storage(&data, 16384);
6303 let config = test_config();
6304
6305 let (atx, amask) = test_alert_channel();
6306 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6307 let handle = TorrentHandle::from_torrent(
6308 meta,
6309 irontide_core::TorrentVersion::V1Only,
6310 None,
6311 dh,
6312 dm,
6313 config,
6314 test_dht_rx(),
6315 test_dht_rx(),
6316 None,
6317 None,
6318 crate::slot_tuner::SlotTuner::disabled(4),
6319 atx,
6320 amask,
6321 None,
6322 None,
6323 test_ban_manager(),
6324 test_ip_filter(),
6325 Arc::new(Vec::new()),
6326 None,
6327 None,
6328 Arc::new(crate::transport::NetworkFactory::tokio()),
6329 None, Arc::new(crate::stats::SessionCounters::new()),
6331 )
6332 .await
6333 .unwrap();
6334
6335 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6337 let addr = listener.local_addr().unwrap();
6338 handle
6339 .add_peers(vec![addr, addr, addr], PeerSource::Tracker)
6340 .await
6341 .unwrap();
6342
6343 tokio::time::sleep(Duration::from_millis(100)).await;
6344 let stats = handle.stats().await.unwrap();
6345 assert!(
6347 stats.peers_available + stats.peers_connected <= 1,
6348 "expected at most 1 unique peer, got available={}, connected={}",
6349 stats.peers_available,
6350 stats.peers_connected
6351 );
6352
6353 handle.shutdown().await.unwrap();
6354 }
6355
6356 #[tokio::test]
6359 async fn cloned_handle_shares_actor() {
6360 let data = vec![0xAB; 16384];
6361 let meta = make_test_torrent(&data, 16384);
6362 let storage = make_storage(&data, 16384);
6363 let config = test_config();
6364
6365 let (atx, amask) = test_alert_channel();
6366 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6367 let handle = TorrentHandle::from_torrent(
6368 meta,
6369 irontide_core::TorrentVersion::V1Only,
6370 None,
6371 dh,
6372 dm,
6373 config,
6374 test_dht_rx(),
6375 test_dht_rx(),
6376 None,
6377 None,
6378 crate::slot_tuner::SlotTuner::disabled(4),
6379 atx,
6380 amask,
6381 None,
6382 None,
6383 test_ban_manager(),
6384 test_ip_filter(),
6385 Arc::new(Vec::new()),
6386 None,
6387 None,
6388 Arc::new(crate::transport::NetworkFactory::tokio()),
6389 None, Arc::new(crate::stats::SessionCounters::new()),
6391 )
6392 .await
6393 .unwrap();
6394 let handle2 = handle.clone();
6395
6396 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6398 let peer_addr = listener.local_addr().unwrap();
6399
6400 handle
6402 .add_peers(vec![peer_addr], PeerSource::Tracker)
6403 .await
6404 .unwrap();
6405
6406 tokio::time::sleep(Duration::from_millis(100)).await;
6407
6408 let stats = handle2.stats().await.unwrap();
6410 assert!(
6411 stats.peers_available + stats.peers_connected >= 1,
6412 "expected at least 1 peer known, got available={}, connected={}",
6413 stats.peers_available,
6414 stats.peers_connected
6415 );
6416
6417 handle.shutdown().await.unwrap();
6418 }
6419
6420 #[tokio::test]
6423 async fn peer_connect_and_disconnect_via_listener() {
6424 let data = vec![0xAB; 16384];
6425 let meta = make_test_torrent(&data, 16384);
6426 let info_hash = meta.info_hash;
6427 let storage = make_storage(&data, 16384);
6428
6429 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6431 let listen_addr = listener.local_addr().unwrap();
6432
6433 let config = TorrentConfig {
6434 listen_port: listen_addr.port(),
6435 ..test_config()
6436 };
6437
6438 drop(listener);
6440
6441 let (atx, amask) = test_alert_channel();
6442 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6443 let handle = TorrentHandle::from_torrent(
6444 meta,
6445 irontide_core::TorrentVersion::V1Only,
6446 None,
6447 dh,
6448 dm,
6449 config,
6450 test_dht_rx(),
6451 test_dht_rx(),
6452 None,
6453 None,
6454 crate::slot_tuner::SlotTuner::disabled(4),
6455 atx,
6456 amask,
6457 None,
6458 None,
6459 test_ban_manager(),
6460 test_ip_filter(),
6461 Arc::new(Vec::new()),
6462 None,
6463 None,
6464 Arc::new(crate::transport::NetworkFactory::tokio()),
6465 None, Arc::new(crate::stats::SessionCounters::new()),
6467 )
6468 .await
6469 .unwrap();
6470
6471 tokio::time::sleep(Duration::from_millis(50)).await;
6473
6474 let mut stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6476
6477 let remote_id = Id20::from_hex("1111111111111111111111111111111111111111").unwrap();
6479 let remote_hs = Handshake::new(info_hash, remote_id);
6480 stream.write_all(&remote_hs.to_bytes()).await.unwrap();
6481 stream.flush().await.unwrap();
6482
6483 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6484 stream.read_exact(&mut hs_buf).await.unwrap();
6485 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
6486 assert_eq!(their_hs.info_hash, info_hash);
6487
6488 tokio::time::sleep(Duration::from_millis(100)).await;
6490
6491 let stats = handle.stats().await.unwrap();
6492 assert_eq!(stats.peers_connected, 1);
6493
6494 drop(stream);
6496
6497 tokio::time::sleep(Duration::from_millis(200)).await;
6499
6500 let stats = handle.stats().await.unwrap();
6501 assert_eq!(stats.peers_connected, 0);
6502
6503 handle.shutdown().await.unwrap();
6504 }
6505
6506 #[tokio::test]
6512 async fn piece_download_and_verify() {
6513 let data = vec![0xCDu8; 16384];
6515 let meta = make_test_torrent(&data, 16384);
6516 let info_hash = meta.info_hash;
6517 let storage = make_storage(&data, 16384);
6518
6519 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6520 let listen_addr = listener.local_addr().unwrap();
6521 drop(listener);
6522
6523 let config = TorrentConfig {
6524 listen_port: listen_addr.port(),
6525 ..test_config()
6526 };
6527
6528 let (atx, amask) = test_alert_channel();
6529 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6530 let handle = TorrentHandle::from_torrent(
6531 meta,
6532 irontide_core::TorrentVersion::V1Only,
6533 None,
6534 dh,
6535 dm,
6536 config,
6537 test_dht_rx(),
6538 test_dht_rx(),
6539 None,
6540 None,
6541 crate::slot_tuner::SlotTuner::disabled(4),
6542 atx,
6543 amask,
6544 None,
6545 None,
6546 test_ban_manager(),
6547 test_ip_filter(),
6548 Arc::new(Vec::new()),
6549 None,
6550 None,
6551 Arc::new(crate::transport::NetworkFactory::tokio()),
6552 None, Arc::new(crate::stats::SessionCounters::new()),
6554 )
6555 .await
6556 .unwrap();
6557
6558 tokio::time::sleep(Duration::from_millis(50)).await;
6559
6560 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6562 let remote_id = Id20::from_hex("2222222222222222222222222222222222222222").unwrap();
6563
6564 let mock_data = data.clone();
6566 let mock_task = tokio::spawn(async move {
6567 let (reader, writer) = tokio::io::split(stream);
6568 let mut reader = reader;
6569 let mut writer = writer;
6570
6571 let hs = Handshake::new(info_hash, remote_id);
6573 writer.write_all(&hs.to_bytes()).await.unwrap();
6574 writer.flush().await.unwrap();
6575
6576 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6577 reader.read_exact(&mut hs_buf).await.unwrap();
6578
6579 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6581 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6582
6583 let _msg = framed_read.next().await;
6585
6586 let ext_hs = ExtHandshake::new();
6588 let payload = ext_hs.to_bytes().unwrap();
6589 framed_write
6590 .send(Message::Extended { ext_id: 0, payload })
6591 .await
6592 .unwrap();
6593
6594 let mut bf = Bitfield::new(1);
6596 bf.set(0);
6597 framed_write
6598 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6599 .await
6600 .unwrap();
6601
6602 framed_write.send(Message::Unchoke).await.unwrap();
6604
6605 while let Some(Ok(msg)) = framed_read.next().await {
6607 if let Message::Request {
6608 index,
6609 begin,
6610 length,
6611 } = msg
6612 {
6613 let start = begin as usize;
6614 let end = start + length as usize;
6615 let piece_data = &mock_data[start..end];
6616 framed_write
6617 .send(Message::Piece {
6618 index,
6619 begin,
6620 data_0: Bytes::copy_from_slice(piece_data),
6621 data_1: Bytes::new(),
6622 })
6623 .await
6624 .unwrap();
6625 }
6626 }
6627 });
6628
6629 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6631 loop {
6632 tokio::time::sleep(Duration::from_millis(100)).await;
6633 let stats = handle.stats().await.unwrap();
6634 if stats.state == TorrentState::Seeding {
6635 assert_eq!(stats.pieces_have, 1);
6636 assert_eq!(stats.pieces_total, 1);
6637 break;
6638 }
6639 if tokio::time::Instant::now() > deadline {
6640 let stats = handle.stats().await.unwrap();
6641 panic!(
6642 "download did not complete within 5s, state={:?}, have={}/{}",
6643 stats.state, stats.pieces_have, stats.pieces_total
6644 );
6645 }
6646 }
6647
6648 handle.shutdown().await.unwrap();
6649 mock_task.abort();
6650 }
6651
6652 #[tokio::test]
6655 async fn failed_piece_verification() {
6656 let data = vec![0xEEu8; 16384];
6658 let meta = make_test_torrent(&data, 16384);
6659 let info_hash = meta.info_hash;
6660 let storage = make_storage(&data, 16384);
6661
6662 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6663 let listen_addr = listener.local_addr().unwrap();
6664 drop(listener);
6665
6666 let config = TorrentConfig {
6667 listen_port: listen_addr.port(),
6668 ..test_config()
6669 };
6670
6671 let (atx, amask) = test_alert_channel();
6672 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6673 let handle = TorrentHandle::from_torrent(
6674 meta,
6675 irontide_core::TorrentVersion::V1Only,
6676 None,
6677 dh,
6678 dm,
6679 config,
6680 test_dht_rx(),
6681 test_dht_rx(),
6682 None,
6683 None,
6684 crate::slot_tuner::SlotTuner::disabled(4),
6685 atx,
6686 amask,
6687 None,
6688 None,
6689 test_ban_manager(),
6690 test_ip_filter(),
6691 Arc::new(Vec::new()),
6692 None,
6693 None,
6694 Arc::new(crate::transport::NetworkFactory::tokio()),
6695 None, Arc::new(crate::stats::SessionCounters::new()),
6697 )
6698 .await
6699 .unwrap();
6700
6701 tokio::time::sleep(Duration::from_millis(50)).await;
6702
6703 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6705 let remote_id = Id20::from_hex("3333333333333333333333333333333333333333").unwrap();
6706
6707 let correct_data = data.clone();
6708 let mock_task = tokio::spawn(async move {
6709 let (reader, writer) = tokio::io::split(stream);
6710
6711 let mut writer = writer;
6713 let mut reader = reader;
6714 let hs = Handshake::new(info_hash, remote_id);
6715 writer.write_all(&hs.to_bytes()).await.unwrap();
6716 writer.flush().await.unwrap();
6717
6718 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6719 reader.read_exact(&mut hs_buf).await.unwrap();
6720
6721 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6722 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6723
6724 let _msg = framed_read.next().await;
6726
6727 let ext_hs = ExtHandshake::new();
6729 let payload = ext_hs.to_bytes().unwrap();
6730 framed_write
6731 .send(Message::Extended { ext_id: 0, payload })
6732 .await
6733 .unwrap();
6734
6735 let mut bf = Bitfield::new(1);
6737 bf.set(0);
6738 framed_write
6739 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6740 .await
6741 .unwrap();
6742
6743 framed_write.send(Message::Unchoke).await.unwrap();
6745
6746 let mut request_count = 0u32;
6747 while let Some(Ok(msg)) = framed_read.next().await {
6748 if let Message::Request {
6749 index,
6750 begin,
6751 length,
6752 } = msg
6753 {
6754 request_count += 1;
6755 let piece_data = if request_count <= 1 {
6756 vec![0xFF; length as usize]
6758 } else {
6759 let start = begin as usize;
6761 let end = start + length as usize;
6762 correct_data[start..end].to_vec()
6763 };
6764 framed_write
6765 .send(Message::Piece {
6766 index,
6767 begin,
6768 data_0: Bytes::from(piece_data),
6769 data_1: Bytes::new(),
6770 })
6771 .await
6772 .unwrap();
6773 }
6774 }
6775 });
6776
6777 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6779 loop {
6780 tokio::time::sleep(Duration::from_millis(100)).await;
6781 let stats = handle.stats().await.unwrap();
6782 if stats.state == TorrentState::Seeding {
6783 assert_eq!(stats.pieces_have, 1);
6784 break;
6785 }
6786 if tokio::time::Instant::now() > deadline {
6787 let stats = handle.stats().await.unwrap();
6788 panic!(
6789 "download did not complete after retry within 5s, state={:?}, have={}",
6790 stats.state, stats.pieces_have,
6791 );
6792 }
6793 }
6794
6795 handle.shutdown().await.unwrap();
6796 mock_task.abort();
6797 }
6798
6799 #[tokio::test]
6802 async fn complete_transitions_state() {
6803 let data = vec![0xBBu8; 32768];
6805 let meta = make_test_torrent(&data, 16384);
6806 let info_hash = meta.info_hash;
6807 let storage = make_storage(&data, 16384);
6808
6809 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6810 let listen_addr = listener.local_addr().unwrap();
6811 drop(listener);
6812
6813 let config = TorrentConfig {
6814 listen_port: listen_addr.port(),
6815 ..test_config()
6816 };
6817
6818 let (atx, amask) = test_alert_channel();
6819 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6820 let handle = TorrentHandle::from_torrent(
6821 meta,
6822 irontide_core::TorrentVersion::V1Only,
6823 None,
6824 dh,
6825 dm,
6826 config,
6827 test_dht_rx(),
6828 test_dht_rx(),
6829 None,
6830 None,
6831 crate::slot_tuner::SlotTuner::disabled(4),
6832 atx,
6833 amask,
6834 None,
6835 None,
6836 test_ban_manager(),
6837 test_ip_filter(),
6838 Arc::new(Vec::new()),
6839 None,
6840 None,
6841 Arc::new(crate::transport::NetworkFactory::tokio()),
6842 None, Arc::new(crate::stats::SessionCounters::new()),
6844 )
6845 .await
6846 .unwrap();
6847
6848 tokio::time::sleep(Duration::from_millis(50)).await;
6849
6850 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6852 let remote_id = Id20::from_hex("4444444444444444444444444444444444444444").unwrap();
6853
6854 let mock_data = data.clone();
6855 let mock_task = tokio::spawn(async move {
6856 let (reader, writer) = tokio::io::split(stream);
6857 let mut writer = writer;
6858 let mut reader = reader;
6859
6860 let hs = Handshake::new(info_hash, remote_id);
6861 writer.write_all(&hs.to_bytes()).await.unwrap();
6862 writer.flush().await.unwrap();
6863
6864 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6865 reader.read_exact(&mut hs_buf).await.unwrap();
6866
6867 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6868 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6869
6870 let _msg = framed_read.next().await;
6872
6873 let ext_hs = ExtHandshake::new();
6875 let payload = ext_hs.to_bytes().unwrap();
6876 framed_write
6877 .send(Message::Extended { ext_id: 0, payload })
6878 .await
6879 .unwrap();
6880
6881 let mut bf = Bitfield::new(2);
6883 bf.set(0);
6884 bf.set(1);
6885 framed_write
6886 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6887 .await
6888 .unwrap();
6889
6890 framed_write.send(Message::Unchoke).await.unwrap();
6891
6892 while let Some(Ok(msg)) = framed_read.next().await {
6893 if let Message::Request {
6894 index,
6895 begin,
6896 length,
6897 } = msg
6898 {
6899 let abs_start = (index as usize * 16384) + begin as usize;
6900 let abs_end = abs_start + length as usize;
6901 let piece_data = &mock_data[abs_start..abs_end];
6902 framed_write
6903 .send(Message::Piece {
6904 index,
6905 begin,
6906 data_0: Bytes::copy_from_slice(piece_data),
6907 data_1: Bytes::new(),
6908 })
6909 .await
6910 .unwrap();
6911 }
6912 }
6913 });
6914
6915 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6916 loop {
6917 tokio::time::sleep(Duration::from_millis(100)).await;
6918 let stats = handle.stats().await.unwrap();
6919 if stats.state == TorrentState::Seeding {
6920 assert_eq!(stats.pieces_have, 2);
6921 assert_eq!(stats.pieces_total, 2);
6922 break;
6923 }
6924 if tokio::time::Instant::now() > deadline {
6925 let stats = handle.stats().await.unwrap();
6926 panic!(
6927 "expected Complete, got {:?}, have={}/{}",
6928 stats.state, stats.pieces_have, stats.pieces_total
6929 );
6930 }
6931 }
6932
6933 handle.shutdown().await.unwrap();
6934 mock_task.abort();
6935 }
6936
6937 #[tokio::test]
6940 async fn multi_chunk_piece_download() {
6941 let data = vec![0xAAu8; 32768];
6943 let meta = make_test_torrent(&data, 32768);
6944 let info_hash = meta.info_hash;
6945 let storage = make_storage(&data, 32768);
6946
6947 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6948 let listen_addr = listener.local_addr().unwrap();
6949 drop(listener);
6950
6951 let config = TorrentConfig {
6952 listen_port: listen_addr.port(),
6953 ..test_config()
6954 };
6955
6956 let (atx, amask) = test_alert_channel();
6957 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6958 let handle = TorrentHandle::from_torrent(
6959 meta,
6960 irontide_core::TorrentVersion::V1Only,
6961 None,
6962 dh,
6963 dm,
6964 config,
6965 test_dht_rx(),
6966 test_dht_rx(),
6967 None,
6968 None,
6969 crate::slot_tuner::SlotTuner::disabled(4),
6970 atx,
6971 amask,
6972 None,
6973 None,
6974 test_ban_manager(),
6975 test_ip_filter(),
6976 Arc::new(Vec::new()),
6977 None,
6978 None,
6979 Arc::new(crate::transport::NetworkFactory::tokio()),
6980 None, Arc::new(crate::stats::SessionCounters::new()),
6982 )
6983 .await
6984 .unwrap();
6985
6986 tokio::time::sleep(Duration::from_millis(50)).await;
6987
6988 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6989 let remote_id = Id20::from_hex("5555555555555555555555555555555555555555").unwrap();
6990
6991 let mock_data = data.clone();
6992 let mock_task = tokio::spawn(async move {
6993 let (reader, writer) = tokio::io::split(stream);
6994 let mut writer = writer;
6995 let mut reader = reader;
6996
6997 let hs = Handshake::new(info_hash, remote_id);
6998 writer.write_all(&hs.to_bytes()).await.unwrap();
6999 writer.flush().await.unwrap();
7000
7001 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7002 reader.read_exact(&mut hs_buf).await.unwrap();
7003
7004 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7005 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7006
7007 let _msg = framed_read.next().await;
7008
7009 let ext_hs = ExtHandshake::new();
7010 let payload = ext_hs.to_bytes().unwrap();
7011 framed_write
7012 .send(Message::Extended { ext_id: 0, payload })
7013 .await
7014 .unwrap();
7015
7016 let mut bf = Bitfield::new(1);
7017 bf.set(0);
7018 framed_write
7019 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7020 .await
7021 .unwrap();
7022
7023 framed_write.send(Message::Unchoke).await.unwrap();
7024
7025 while let Some(Ok(msg)) = framed_read.next().await {
7026 if let Message::Request {
7027 index: _,
7028 begin,
7029 length,
7030 } = msg
7031 {
7032 let start = begin as usize;
7033 let end = start + length as usize;
7034 framed_write
7035 .send(Message::Piece {
7036 index: 0,
7037 begin,
7038 data_0: Bytes::copy_from_slice(&mock_data[start..end]),
7039 data_1: Bytes::new(),
7040 })
7041 .await
7042 .unwrap();
7043 }
7044 }
7045 });
7046
7047 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7048 loop {
7049 tokio::time::sleep(Duration::from_millis(100)).await;
7050 let stats = handle.stats().await.unwrap();
7051 if stats.state == TorrentState::Seeding {
7052 assert_eq!(stats.pieces_have, 1);
7053 break;
7054 }
7055 assert!(
7056 tokio::time::Instant::now() <= deadline,
7057 "multi-chunk download did not complete within 5s"
7058 );
7059 }
7060
7061 handle.shutdown().await.unwrap();
7062 mock_task.abort();
7063 }
7064
7065 #[tokio::test]
7068 async fn seeder_leecher_integration() {
7069 let data = vec![0xDDu8; 32768]; let piece_length = 16384u64;
7072 let meta = make_test_torrent(&data, piece_length);
7073 let info_hash = meta.info_hash;
7074
7075 let seeder_storage = make_seeded_storage(&data, piece_length);
7077
7078 let seeder_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7084 let seeder_addr = seeder_listener.local_addr().unwrap();
7085
7086 let seeder_task = tokio::spawn(async move {
7087 let (stream, _addr) = seeder_listener.accept().await.unwrap();
7088 let (reader, writer) = tokio::io::split(stream);
7089 let mut writer = writer;
7090 let mut reader = reader;
7091
7092 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7094 reader.read_exact(&mut hs_buf).await.unwrap();
7095 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
7096 assert_eq!(their_hs.info_hash, info_hash);
7097
7098 let hs = Handshake::new(info_hash, PeerId::generate().0);
7099 writer.write_all(&hs.to_bytes()).await.unwrap();
7100 writer.flush().await.unwrap();
7101
7102 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7103 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7104
7105 let _msg = framed_read.next().await;
7107
7108 let ext_hs = ExtHandshake::new();
7110 let payload = ext_hs.to_bytes().unwrap();
7111 framed_write
7112 .send(Message::Extended { ext_id: 0, payload })
7113 .await
7114 .unwrap();
7115
7116 let mut bf = Bitfield::new(2);
7118 bf.set(0);
7119 bf.set(1);
7120 framed_write
7121 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7122 .await
7123 .unwrap();
7124
7125 framed_write.send(Message::Unchoke).await.unwrap();
7127
7128 while let Some(Ok(msg)) = framed_read.next().await {
7130 if let Message::Request {
7131 index,
7132 begin,
7133 length,
7134 } = msg
7135 {
7136 let piece_data = seeder_storage.read_chunk(index, begin, length).unwrap();
7137 framed_write
7138 .send(Message::Piece {
7139 index,
7140 begin,
7141 data_0: Bytes::from(piece_data),
7142 data_1: Bytes::new(),
7143 })
7144 .await
7145 .unwrap();
7146 }
7147 }
7148 });
7149
7150 let leecher_storage = make_storage(&data, piece_length);
7152 let leecher_meta = make_test_torrent(&data, piece_length);
7153
7154 let leecher_config = test_config();
7155 let (latx, lamask) = test_alert_channel();
7156 let (ldh, ldm, _ldj) = test_register_disk(leecher_meta.info_hash, leecher_storage).await;
7157 let leecher = TorrentHandle::from_torrent(
7158 leecher_meta,
7159 irontide_core::TorrentVersion::V1Only,
7160 None,
7161 ldh,
7162 ldm,
7163 leecher_config,
7164 test_dht_rx(),
7165 test_dht_rx(),
7166 None,
7167 None,
7168 crate::slot_tuner::SlotTuner::disabled(4),
7169 latx,
7170 lamask,
7171 None,
7172 None,
7173 test_ban_manager(),
7174 test_ip_filter(),
7175 Arc::new(Vec::new()),
7176 None,
7177 None,
7178 Arc::new(crate::transport::NetworkFactory::tokio()),
7179 None, Arc::new(crate::stats::SessionCounters::new()),
7181 )
7182 .await
7183 .unwrap();
7184
7185 leecher
7187 .add_peers(vec![seeder_addr], PeerSource::Tracker)
7188 .await
7189 .unwrap();
7190
7191 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
7195 loop {
7196 tokio::time::sleep(Duration::from_millis(200)).await;
7197 let stats = leecher.stats().await.unwrap();
7198 if stats.state == TorrentState::Seeding {
7199 assert_eq!(stats.pieces_have, 2);
7200 assert_eq!(stats.pieces_total, 2);
7201 break;
7202 }
7203 if tokio::time::Instant::now() > deadline {
7204 let stats = leecher.stats().await.unwrap();
7205 panic!(
7206 "seeder/leecher: leecher did not complete, state={:?}, have={}/{}, connected={}, available={}",
7207 stats.state,
7208 stats.pieces_have,
7209 stats.pieces_total,
7210 stats.peers_connected,
7211 stats.peers_available,
7212 );
7213 }
7214 }
7215
7216 leecher.shutdown().await.unwrap();
7217 seeder_task.abort();
7218 }
7219
7220 #[tokio::test]
7223 async fn magnet_initial_stats() {
7224 let magnet = Magnet {
7225 info_hashes: irontide_core::InfoHashes::v1_only(
7226 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
7227 ),
7228 display_name: Some("magnet test".into()),
7229 trackers: vec![],
7230 peers: vec![],
7231 selected_files: None,
7232 };
7233
7234 let (atx, amask) = test_alert_channel();
7235 let (dm, _dj) = test_disk_manager();
7236 let handle = TorrentHandle::from_magnet(
7237 magnet,
7238 dm,
7239 test_config(),
7240 test_dht_rx(),
7241 test_dht_rx(),
7242 None,
7243 None,
7244 crate::slot_tuner::SlotTuner::disabled(4),
7245 atx,
7246 amask,
7247 None,
7248 None,
7249 test_ban_manager(),
7250 test_ip_filter(),
7251 Arc::new(Vec::new()),
7252 None,
7253 None,
7254 Arc::new(crate::transport::NetworkFactory::tokio()),
7255 None, Arc::new(crate::stats::SessionCounters::new()),
7257 )
7258 .await
7259 .unwrap();
7260
7261 let stats = handle.stats().await.unwrap();
7262 assert_eq!(stats.state, TorrentState::FetchingMetadata);
7263 assert_eq!(stats.pieces_total, 0);
7264 assert_eq!(stats.pieces_have, 0);
7265 assert_eq!(stats.downloaded, 0);
7266 assert_eq!(stats.uploaded, 0);
7267 assert_eq!(stats.peers_connected, 0);
7268 assert_eq!(stats.peers_available, 0);
7269
7270 handle.shutdown().await.unwrap();
7271 }
7272
7273 #[tokio::test]
7276 async fn tracker_populated_from_metadata() {
7277 use serde::Serialize;
7278
7279 #[derive(Serialize)]
7280 struct Info<'a> {
7281 length: u64,
7282 name: &'a str,
7283 #[serde(rename = "piece length")]
7284 piece_length: u64,
7285 #[serde(with = "serde_bytes")]
7286 pieces: &'a [u8],
7287 }
7288
7289 #[derive(Serialize)]
7290 struct Torrent<'a> {
7291 announce: &'a str,
7292 info: Info<'a>,
7293 }
7294
7295 let data = vec![0xAB; 16384];
7296 let hash = irontide_core::sha1(&data);
7297 let mut pieces = Vec::new();
7298 pieces.extend_from_slice(hash.as_bytes());
7299
7300 let t = Torrent {
7301 announce: "http://tracker.example.com:8080/announce",
7302 info: Info {
7303 length: data.len() as u64,
7304 name: "test",
7305 piece_length: 16384,
7306 pieces: &pieces,
7307 },
7308 };
7309
7310 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7311 let meta = torrent_from_bytes(&bytes).unwrap();
7312 assert!(meta.announce.is_some());
7313
7314 let storage = make_storage(&data, 16384);
7315 let config = test_config();
7316
7317 let (atx, amask) = test_alert_channel();
7320 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7321 let handle = TorrentHandle::from_torrent(
7322 meta,
7323 irontide_core::TorrentVersion::V1Only,
7324 None,
7325 dh,
7326 dm,
7327 config,
7328 test_dht_rx(),
7329 test_dht_rx(),
7330 None,
7331 None,
7332 crate::slot_tuner::SlotTuner::disabled(4),
7333 atx,
7334 amask,
7335 None,
7336 None,
7337 test_ban_manager(),
7338 test_ip_filter(),
7339 Arc::new(Vec::new()),
7340 None,
7341 None,
7342 Arc::new(crate::transport::NetworkFactory::tokio()),
7343 None, Arc::new(crate::stats::SessionCounters::new()),
7345 )
7346 .await
7347 .unwrap();
7348
7349 let stats = handle.stats().await.unwrap();
7350 assert_eq!(stats.state, TorrentState::Downloading);
7351
7352 handle.shutdown().await.unwrap();
7353 }
7354
7355 #[tokio::test]
7358 async fn private_torrent_no_dht_field() {
7359 use serde::Serialize;
7360
7361 #[derive(Serialize)]
7362 struct Info<'a> {
7363 length: u64,
7364 name: &'a str,
7365 #[serde(rename = "piece length")]
7366 piece_length: u64,
7367 #[serde(with = "serde_bytes")]
7368 pieces: &'a [u8],
7369 private: i64,
7370 }
7371
7372 #[derive(Serialize)]
7373 struct Torrent<'a> {
7374 announce: &'a str,
7375 info: Info<'a>,
7376 }
7377
7378 let data = vec![0xAB; 16384];
7379 let hash = irontide_core::sha1(&data);
7380 let mut pieces = Vec::new();
7381 pieces.extend_from_slice(hash.as_bytes());
7382
7383 let t = Torrent {
7384 announce: "http://private-tracker.example.com/announce",
7385 info: Info {
7386 length: data.len() as u64,
7387 name: "private_test",
7388 piece_length: 16384,
7389 pieces: &pieces,
7390 private: 1,
7391 },
7392 };
7393
7394 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7395 let meta = torrent_from_bytes(&bytes).unwrap();
7396 assert_eq!(meta.info.private, Some(1));
7397
7398 let storage = make_storage(&data, 16384);
7399 let config = test_config();
7400
7401 let (atx, amask) = test_alert_channel();
7402 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7403 let handle = TorrentHandle::from_torrent(
7404 meta,
7405 irontide_core::TorrentVersion::V1Only,
7406 None,
7407 dh,
7408 dm,
7409 config,
7410 test_dht_rx(),
7411 test_dht_rx(),
7412 None,
7413 None,
7414 crate::slot_tuner::SlotTuner::disabled(4),
7415 atx,
7416 amask,
7417 None,
7418 None,
7419 test_ban_manager(),
7420 test_ip_filter(),
7421 Arc::new(Vec::new()),
7422 None,
7423 None,
7424 Arc::new(crate::transport::NetworkFactory::tokio()),
7425 None, Arc::new(crate::stats::SessionCounters::new()),
7427 )
7428 .await
7429 .unwrap();
7430
7431 let stats = handle.stats().await.unwrap();
7432 assert_eq!(stats.state, TorrentState::Downloading);
7433
7434 handle.shutdown().await.unwrap();
7435 }
7436
7437 #[tokio::test]
7440 async fn magnet_no_tracker_before_metadata() {
7441 let magnet = Magnet {
7442 info_hashes: irontide_core::InfoHashes::v1_only(
7443 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
7444 ),
7445 display_name: Some("magnet test".into()),
7446 trackers: vec![],
7447 peers: vec![],
7448 selected_files: None,
7449 };
7450
7451 let (atx, amask) = test_alert_channel();
7452 let (dm, _dj) = test_disk_manager();
7453 let handle = TorrentHandle::from_magnet(
7454 magnet,
7455 dm,
7456 test_config(),
7457 test_dht_rx(),
7458 test_dht_rx(),
7459 None,
7460 None,
7461 crate::slot_tuner::SlotTuner::disabled(4),
7462 atx,
7463 amask,
7464 None,
7465 None,
7466 test_ban_manager(),
7467 test_ip_filter(),
7468 Arc::new(Vec::new()),
7469 None,
7470 None,
7471 Arc::new(crate::transport::NetworkFactory::tokio()),
7472 None, Arc::new(crate::stats::SessionCounters::new()),
7474 )
7475 .await
7476 .unwrap();
7477
7478 let stats = handle.stats().await.unwrap();
7479 assert_eq!(stats.state, TorrentState::FetchingMetadata);
7480
7481 tokio::time::sleep(Duration::from_millis(50)).await;
7485
7486 handle.shutdown().await.unwrap();
7487 }
7488
7489 #[tokio::test]
7492 async fn pause_and_resume() {
7493 let data = vec![0xEEu8; 32768];
7494 let meta = make_test_torrent(&data, 16384);
7495 let storage = make_storage(&data, 16384);
7496 let config = test_config();
7497 let (atx, amask) = test_alert_channel();
7498 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7499 let handle = TorrentHandle::from_torrent(
7500 meta,
7501 irontide_core::TorrentVersion::V1Only,
7502 None,
7503 dh,
7504 dm,
7505 config,
7506 test_dht_rx(),
7507 test_dht_rx(),
7508 None,
7509 None,
7510 crate::slot_tuner::SlotTuner::disabled(4),
7511 atx,
7512 amask,
7513 None,
7514 None,
7515 test_ban_manager(),
7516 test_ip_filter(),
7517 Arc::new(Vec::new()),
7518 None,
7519 None,
7520 Arc::new(crate::transport::NetworkFactory::tokio()),
7521 None, Arc::new(crate::stats::SessionCounters::new()),
7523 )
7524 .await
7525 .unwrap();
7526
7527 let stats = handle.stats().await.unwrap();
7528 assert_eq!(stats.state, TorrentState::Downloading);
7529
7530 handle.pause().await.unwrap();
7531 tokio::time::sleep(Duration::from_millis(50)).await;
7532 let stats = handle.stats().await.unwrap();
7533 assert_eq!(stats.state, TorrentState::Paused);
7534
7535 handle.resume().await.unwrap();
7536 tokio::time::sleep(Duration::from_millis(50)).await;
7537 let stats = handle.stats().await.unwrap();
7538 assert_eq!(stats.state, TorrentState::Downloading);
7539
7540 handle.shutdown().await.unwrap();
7541 }
7542
7543 #[tokio::test]
7546 async fn pause_already_paused_is_noop() {
7547 let data = vec![0xEEu8; 32768];
7548 let meta = make_test_torrent(&data, 16384);
7549 let storage = make_storage(&data, 16384);
7550 let config = test_config();
7551 let (atx, amask) = test_alert_channel();
7552 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7553 let handle = TorrentHandle::from_torrent(
7554 meta,
7555 irontide_core::TorrentVersion::V1Only,
7556 None,
7557 dh,
7558 dm,
7559 config,
7560 test_dht_rx(),
7561 test_dht_rx(),
7562 None,
7563 None,
7564 crate::slot_tuner::SlotTuner::disabled(4),
7565 atx,
7566 amask,
7567 None,
7568 None,
7569 test_ban_manager(),
7570 test_ip_filter(),
7571 Arc::new(Vec::new()),
7572 None,
7573 None,
7574 Arc::new(crate::transport::NetworkFactory::tokio()),
7575 None, Arc::new(crate::stats::SessionCounters::new()),
7577 )
7578 .await
7579 .unwrap();
7580
7581 handle.pause().await.unwrap();
7582 tokio::time::sleep(Duration::from_millis(50)).await;
7583 handle.pause().await.unwrap(); tokio::time::sleep(Duration::from_millis(50)).await;
7585 let stats = handle.stats().await.unwrap();
7586 assert_eq!(stats.state, TorrentState::Paused);
7587
7588 handle.shutdown().await.unwrap();
7589 }
7590
7591 #[tokio::test]
7597 async fn incoming_request_served_from_storage() {
7598 let data = vec![0xABu8; 16384];
7599 let meta = make_test_torrent(&data, 16384);
7600 let info_hash = meta.info_hash;
7601 let storage = make_storage(&data, 16384);
7602
7603 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7604 let listen_addr = listener.local_addr().unwrap();
7605 drop(listener);
7606
7607 let config = TorrentConfig {
7608 listen_port: listen_addr.port(),
7609 ..test_config()
7610 };
7611
7612 let (atx, amask) = test_alert_channel();
7613 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7614 let handle = TorrentHandle::from_torrent(
7615 meta,
7616 irontide_core::TorrentVersion::V1Only,
7617 None,
7618 dh,
7619 dm,
7620 config,
7621 test_dht_rx(),
7622 test_dht_rx(),
7623 None,
7624 None,
7625 crate::slot_tuner::SlotTuner::disabled(4),
7626 atx,
7627 amask,
7628 None,
7629 None,
7630 test_ban_manager(),
7631 test_ip_filter(),
7632 Arc::new(Vec::new()),
7633 None,
7634 None,
7635 Arc::new(crate::transport::NetworkFactory::tokio()),
7636 None, Arc::new(crate::stats::SessionCounters::new()),
7638 )
7639 .await
7640 .unwrap();
7641
7642 tokio::time::sleep(Duration::from_millis(50)).await;
7643
7644 let seed_data = data.clone();
7646 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7647 let seeder_task = tokio::spawn(async move {
7648 let (reader, writer) = tokio::io::split(seed_stream);
7649 let mut writer = writer;
7650 let mut reader = reader;
7651
7652 let hs = Handshake::new(
7653 info_hash,
7654 Id20::from_hex("6666666666666666666666666666666666666666").unwrap(),
7655 );
7656 writer.write_all(&hs.to_bytes()).await.unwrap();
7657 writer.flush().await.unwrap();
7658 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7659 reader.read_exact(&mut hs_buf).await.unwrap();
7660
7661 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7662 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7663
7664 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
7666 let payload = ext_hs.to_bytes().unwrap();
7667 framed_write
7668 .send(Message::Extended { ext_id: 0, payload })
7669 .await
7670 .unwrap();
7671
7672 let mut bf = Bitfield::new(1);
7674 bf.set(0);
7675 framed_write
7676 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7677 .await
7678 .unwrap();
7679 framed_write.send(Message::Unchoke).await.unwrap();
7680
7681 while let Some(Ok(msg)) = framed_read.next().await {
7683 if let Message::Request {
7684 index,
7685 begin,
7686 length,
7687 } = msg
7688 {
7689 let start = begin as usize;
7690 let end = start + length as usize;
7691 framed_write
7692 .send(Message::Piece {
7693 index,
7694 begin,
7695 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
7696 data_1: Bytes::new(),
7697 })
7698 .await
7699 .unwrap();
7700 }
7701 }
7702 });
7703
7704 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7706 loop {
7707 tokio::time::sleep(Duration::from_millis(100)).await;
7708 let stats = handle.stats().await.unwrap();
7709 if stats.pieces_have == 1 {
7710 break;
7711 }
7712 assert!(
7713 tokio::time::Instant::now() <= deadline,
7714 "piece download did not complete within 5s"
7715 );
7716 }
7717
7718 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7720 let expected_data = data.clone();
7721 let leecher_task = tokio::spawn(async move {
7722 let (reader, writer) = tokio::io::split(leech_stream);
7723 let mut writer = writer;
7724 let mut reader = reader;
7725
7726 let hs = Handshake::new(
7727 info_hash,
7728 Id20::from_hex("7777777777777777777777777777777777777777").unwrap(),
7729 );
7730 writer.write_all(&hs.to_bytes()).await.unwrap();
7731 writer.flush().await.unwrap();
7732 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7733 reader.read_exact(&mut hs_buf).await.unwrap();
7734
7735 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7736 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7737
7738 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
7740 let payload = ext_hs.to_bytes().unwrap();
7741 framed_write
7742 .send(Message::Extended { ext_id: 0, payload })
7743 .await
7744 .unwrap();
7745
7746 framed_write.send(Message::Interested).await.unwrap();
7748
7749 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
7750 loop {
7751 tokio::select! {
7752 msg = framed_read.next() => {
7753 match msg {
7754 Some(Ok(Message::Unchoke)) => { break; }
7755 Some(Ok(_)) => {}
7756 _ => panic!("connection closed before unchoke"),
7757 }
7758 }
7759 () = tokio::time::sleep_until(deadline) => {
7760 panic!("timed out waiting for unchoke");
7761 }
7762 }
7763 }
7764
7765 framed_write
7767 .send(Message::Request {
7768 index: 0,
7769 begin: 0,
7770 length: 16384,
7771 })
7772 .await
7773 .unwrap();
7774
7775 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7777 loop {
7778 tokio::select! {
7779 msg = framed_read.next() => {
7780 match msg {
7781 Some(Ok(Message::Piece { index, begin, data_0, data_1 })) => {
7782 assert_eq!(index, 0);
7783 assert_eq!(begin, 0);
7784 let _ = &data_1; assert_eq!(data_0.as_ref(), expected_data.as_slice());
7786 return; }
7788 Some(Ok(_)) => {}
7789 Some(Err(e)) => panic!("error reading: {e}"),
7790 None => panic!("connection closed before piece"),
7791 }
7792 }
7793 () = tokio::time::sleep_until(deadline) => {
7794 panic!("timed out waiting for piece data");
7795 }
7796 }
7797 }
7798 });
7799
7800 let result = tokio::time::timeout(Duration::from_secs(20), leecher_task).await;
7802 match result {
7803 Ok(Ok(())) => {}
7804 Ok(Err(e)) => panic!("leecher task panicked: {e}"),
7805 Err(elapsed) => panic!("test timed out after {elapsed}"),
7806 }
7807
7808 let stats = handle.stats().await.unwrap();
7810 assert!(
7811 stats.uploaded > 0,
7812 "expected uploaded > 0, got {}",
7813 stats.uploaded
7814 );
7815
7816 handle.shutdown().await.unwrap();
7817 seeder_task.abort();
7818 }
7819
7820 #[tokio::test]
7823 async fn seed_ratio_limit_stops_torrent() {
7824 let data = vec![0xCCu8; 16384];
7827 let meta = make_test_torrent(&data, 16384);
7828 let info_hash = meta.info_hash;
7829 let storage = make_storage(&data, 16384);
7830
7831 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7832 let listen_addr = listener.local_addr().unwrap();
7833 drop(listener);
7834
7835 let config = TorrentConfig {
7836 listen_port: listen_addr.port(),
7837 seed_ratio_limit: Some(1.0),
7838 ..test_config()
7839 };
7840
7841 let (atx, amask) = test_alert_channel();
7842 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7843 let handle = TorrentHandle::from_torrent(
7844 meta,
7845 irontide_core::TorrentVersion::V1Only,
7846 None,
7847 dh,
7848 dm,
7849 config,
7850 test_dht_rx(),
7851 test_dht_rx(),
7852 None,
7853 None,
7854 crate::slot_tuner::SlotTuner::disabled(4),
7855 atx,
7856 amask,
7857 None,
7858 None,
7859 test_ban_manager(),
7860 test_ip_filter(),
7861 Arc::new(Vec::new()),
7862 None,
7863 None,
7864 Arc::new(crate::transport::NetworkFactory::tokio()),
7865 None, Arc::new(crate::stats::SessionCounters::new()),
7867 )
7868 .await
7869 .unwrap();
7870
7871 tokio::time::sleep(Duration::from_millis(50)).await;
7872
7873 let seed_data = data.clone();
7875 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7876 let seeder_task = tokio::spawn(async move {
7877 let (reader, writer) = tokio::io::split(seed_stream);
7878 let mut writer = writer;
7879 let mut reader = reader;
7880
7881 let hs = Handshake::new(
7882 info_hash,
7883 Id20::from_hex("8888888888888888888888888888888888888888").unwrap(),
7884 );
7885 writer.write_all(&hs.to_bytes()).await.unwrap();
7886 writer.flush().await.unwrap();
7887 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7888 reader.read_exact(&mut hs_buf).await.unwrap();
7889
7890 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7891 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7892
7893 let _msg = framed_read.next().await;
7894 let ext_hs = ExtHandshake::new();
7895 let payload = ext_hs.to_bytes().unwrap();
7896 framed_write
7897 .send(Message::Extended { ext_id: 0, payload })
7898 .await
7899 .unwrap();
7900
7901 let mut bf = Bitfield::new(1);
7902 bf.set(0);
7903 framed_write
7904 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7905 .await
7906 .unwrap();
7907 framed_write.send(Message::Unchoke).await.unwrap();
7908
7909 while let Some(Ok(msg)) = framed_read.next().await {
7910 if let Message::Request {
7911 index,
7912 begin,
7913 length,
7914 } = msg
7915 {
7916 let start = begin as usize;
7917 let end = start + length as usize;
7918 framed_write
7919 .send(Message::Piece {
7920 index,
7921 begin,
7922 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
7923 data_1: Bytes::new(),
7924 })
7925 .await
7926 .unwrap();
7927 }
7928 }
7929 });
7930
7931 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7933 loop {
7934 tokio::time::sleep(Duration::from_millis(100)).await;
7935 let stats = handle.stats().await.unwrap();
7936 if stats.state == TorrentState::Seeding {
7937 break;
7938 }
7939 assert!(
7940 tokio::time::Instant::now() <= deadline,
7941 "download did not complete within 5s"
7942 );
7943 }
7944
7945 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7947 let leecher_task = tokio::spawn(async move {
7948 let (reader, writer) = tokio::io::split(leech_stream);
7949 let mut writer = writer;
7950 let mut reader = reader;
7951
7952 let hs = Handshake::new(
7953 info_hash,
7954 Id20::from_hex("9999999999999999999999999999999999999999").unwrap(),
7955 );
7956 writer.write_all(&hs.to_bytes()).await.unwrap();
7957 writer.flush().await.unwrap();
7958 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7959 reader.read_exact(&mut hs_buf).await.unwrap();
7960
7961 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7962 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7963
7964 let _msg = framed_read.next().await;
7965 let ext_hs = ExtHandshake::new();
7966 let payload = ext_hs.to_bytes().unwrap();
7967 framed_write
7968 .send(Message::Extended { ext_id: 0, payload })
7969 .await
7970 .unwrap();
7971
7972 framed_write.send(Message::Interested).await.unwrap();
7973
7974 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
7976 loop {
7977 tokio::select! {
7978 msg = framed_read.next() => {
7979 match msg {
7980 Some(Ok(Message::Unchoke)) => break,
7981 Some(Ok(_)) => {}
7982 _ => return, }
7984 }
7985 () = tokio::time::sleep_until(deadline) => return,
7986 }
7987 }
7988
7989 framed_write
7991 .send(Message::Request {
7992 index: 0,
7993 begin: 0,
7994 length: 16384,
7995 })
7996 .await
7997 .unwrap();
7998
7999 while let Some(Ok(_msg)) = framed_read.next().await {}
8001 });
8002
8003 let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
8005 loop {
8006 tokio::time::sleep(Duration::from_millis(100)).await;
8007 let stats = handle.stats().await.unwrap();
8008 if stats.state == TorrentState::Stopped {
8009 assert!(
8010 stats.uploaded >= 16384,
8011 "expected uploaded >= 16384, got {}",
8012 stats.uploaded
8013 );
8014 break;
8015 }
8016 if tokio::time::Instant::now() > deadline {
8017 let stats = handle.stats().await.unwrap();
8018 panic!(
8019 "expected Stopped, got {:?}, uploaded={}, downloaded={}",
8020 stats.state, stats.uploaded, stats.downloaded
8021 );
8022 }
8023 }
8024
8025 handle.shutdown().await.unwrap();
8026 seeder_task.abort();
8027 leecher_task.abort();
8028 }
8029
8030 #[tokio::test]
8033 async fn resume_with_seeded_storage() {
8034 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
8036 let storage = make_seeded_storage(&data, 16384);
8037 let config = test_config();
8038
8039 let (atx, amask) = test_alert_channel();
8040 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8041 let handle = TorrentHandle::from_torrent(
8042 meta,
8043 irontide_core::TorrentVersion::V1Only,
8044 None,
8045 dh,
8046 dm,
8047 config,
8048 test_dht_rx(),
8049 test_dht_rx(),
8050 None,
8051 None,
8052 crate::slot_tuner::SlotTuner::disabled(4),
8053 atx,
8054 amask,
8055 None,
8056 None,
8057 test_ban_manager(),
8058 test_ip_filter(),
8059 Arc::new(Vec::new()),
8060 None,
8061 None,
8062 Arc::new(crate::transport::NetworkFactory::tokio()),
8063 None, Arc::new(crate::stats::SessionCounters::new()),
8065 )
8066 .await
8067 .unwrap();
8068
8069 tokio::time::sleep(Duration::from_millis(100)).await;
8071
8072 let stats = handle.stats().await.unwrap();
8073 assert_eq!(
8074 stats.state,
8075 TorrentState::Seeding,
8076 "should start as seeder with all pieces verified"
8077 );
8078 assert_eq!(stats.pieces_have, 2);
8079 assert_eq!(stats.pieces_total, 2);
8080
8081 handle.shutdown().await.unwrap();
8082 }
8083
8084 #[tokio::test]
8087 async fn save_resume_data_captures_state() {
8088 let data = vec![0xAB; 32768];
8089 let meta = make_test_torrent(&data, 16384);
8090 let info_hash = meta.info_hash;
8091 let storage = make_storage(&data, 16384);
8092 let config = test_config();
8093
8094 let (atx, amask) = test_alert_channel();
8095 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8096 let handle = TorrentHandle::from_torrent(
8097 meta,
8098 irontide_core::TorrentVersion::V1Only,
8099 None,
8100 dh,
8101 dm,
8102 config,
8103 test_dht_rx(),
8104 test_dht_rx(),
8105 None,
8106 None,
8107 crate::slot_tuner::SlotTuner::disabled(4),
8108 atx,
8109 amask,
8110 None,
8111 None,
8112 test_ban_manager(),
8113 test_ip_filter(),
8114 Arc::new(Vec::new()),
8115 None,
8116 None,
8117 Arc::new(crate::transport::NetworkFactory::tokio()),
8118 None, Arc::new(crate::stats::SessionCounters::new()),
8120 )
8121 .await
8122 .unwrap();
8123
8124 tokio::time::sleep(Duration::from_millis(50)).await;
8126
8127 let rd = handle.save_resume_data().await.unwrap();
8128
8129 assert_eq!(rd.file_format, "libtorrent resume file");
8130 assert_eq!(rd.file_version, 1);
8131 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
8132 assert_eq!(rd.name, "test");
8133 assert_eq!(rd.save_path, "/tmp");
8134 assert_eq!(rd.paused, 0);
8135 assert!(!rd.pieces.is_empty());
8137 assert_eq!(rd.total_uploaded, 0);
8139 assert_eq!(rd.total_downloaded, 0);
8140
8141 handle.shutdown().await.unwrap();
8142 }
8143
8144 #[tokio::test]
8147 async fn save_resume_data_seeder() {
8148 let data = vec![0xCD; 32768];
8149 let meta = make_test_torrent(&data, 16384);
8150 let info_hash = meta.info_hash;
8151 let storage = make_seeded_storage(&data, 16384);
8152 let config = test_config();
8153
8154 let (atx, amask) = test_alert_channel();
8155 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8156 let handle = TorrentHandle::from_torrent(
8157 meta,
8158 irontide_core::TorrentVersion::V1Only,
8159 None,
8160 dh,
8161 dm,
8162 config,
8163 test_dht_rx(),
8164 test_dht_rx(),
8165 None,
8166 None,
8167 crate::slot_tuner::SlotTuner::disabled(4),
8168 atx,
8169 amask,
8170 None,
8171 None,
8172 test_ban_manager(),
8173 test_ip_filter(),
8174 Arc::new(Vec::new()),
8175 None,
8176 None,
8177 Arc::new(crate::transport::NetworkFactory::tokio()),
8178 None, Arc::new(crate::stats::SessionCounters::new()),
8180 )
8181 .await
8182 .unwrap();
8183
8184 tokio::time::sleep(Duration::from_millis(100)).await;
8186
8187 let rd = handle.save_resume_data().await.unwrap();
8188
8189 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
8190 assert_eq!(rd.name, "test");
8191 assert_eq!(rd.seed_mode, 1, "seeder should have seed_mode=1");
8192 assert_eq!(rd.paused, 0);
8193 assert_eq!(rd.pieces.len(), 1);
8196 assert_eq!(
8197 rd.pieces[0] & 0xC0,
8198 0xC0,
8199 "both pieces should be marked complete"
8200 );
8201
8202 handle.shutdown().await.unwrap();
8203 }
8204
8205 #[tokio::test]
8208 async fn save_resume_data_paused() {
8209 let data = vec![0xEF; 16384];
8210 let meta = make_test_torrent(&data, 16384);
8211 let storage = make_storage(&data, 16384);
8212 let config = test_config();
8213
8214 let (atx, amask) = test_alert_channel();
8215 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8216 let handle = TorrentHandle::from_torrent(
8217 meta,
8218 irontide_core::TorrentVersion::V1Only,
8219 None,
8220 dh,
8221 dm,
8222 config,
8223 test_dht_rx(),
8224 test_dht_rx(),
8225 None,
8226 None,
8227 crate::slot_tuner::SlotTuner::disabled(4),
8228 atx,
8229 amask,
8230 None,
8231 None,
8232 test_ban_manager(),
8233 test_ip_filter(),
8234 Arc::new(Vec::new()),
8235 None,
8236 None,
8237 Arc::new(crate::transport::NetworkFactory::tokio()),
8238 None, Arc::new(crate::stats::SessionCounters::new()),
8240 )
8241 .await
8242 .unwrap();
8243
8244 tokio::time::sleep(Duration::from_millis(50)).await;
8245 handle.pause().await.unwrap();
8246 tokio::time::sleep(Duration::from_millis(50)).await;
8247
8248 let rd = handle.save_resume_data().await.unwrap();
8249 assert_eq!(rd.paused, 1, "paused torrent should have paused=1");
8250 assert_eq!(rd.seed_mode, 0);
8251
8252 handle.shutdown().await.unwrap();
8253 }
8254
8255 #[tokio::test]
8258 async fn set_file_priority_and_read_back() {
8259 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
8260 let mut torrent_bytes = b"d4:info".to_vec();
8261 torrent_bytes.extend_from_slice(info_bytes);
8262 torrent_bytes.push(b'e');
8263
8264 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
8265 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
8266 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8267 let config = TorrentConfig {
8268 listen_port: 0,
8269 ..Default::default()
8270 };
8271
8272 let (atx, amask) = test_alert_channel();
8273 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8274 let handle = TorrentHandle::from_torrent(
8275 meta,
8276 irontide_core::TorrentVersion::V1Only,
8277 None,
8278 dh,
8279 dm,
8280 config,
8281 test_dht_rx(),
8282 test_dht_rx(),
8283 None,
8284 None,
8285 crate::slot_tuner::SlotTuner::disabled(4),
8286 atx,
8287 amask,
8288 None,
8289 None,
8290 test_ban_manager(),
8291 test_ip_filter(),
8292 Arc::new(Vec::new()),
8293 None,
8294 None,
8295 Arc::new(crate::transport::NetworkFactory::tokio()),
8296 None, Arc::new(crate::stats::SessionCounters::new()),
8298 )
8299 .await
8300 .unwrap();
8301
8302 let prios = handle.file_priorities().await.unwrap();
8304 assert_eq!(prios.len(), 2);
8305 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
8306
8307 handle
8309 .set_file_priority(0, FilePriority::Skip)
8310 .await
8311 .unwrap();
8312
8313 let prios = handle.file_priorities().await.unwrap();
8314 assert_eq!(prios[0], FilePriority::Skip);
8315 assert_eq!(prios[1], FilePriority::Normal);
8316
8317 let result = handle.set_file_priority(99, FilePriority::High).await;
8319 assert!(result.is_err());
8320
8321 handle.shutdown().await.unwrap();
8322 tokio::time::sleep(Duration::from_millis(50)).await;
8323 }
8324
8325 async fn spawn_test_torrent_multifile() -> TorrentHandle {
8329 let meta = make_multi_file_meta(&[(100, "a.bin"), (150, "b.bin"), (100, "c.bin")], 100);
8330 let lengths = Lengths::new(350, 100, DEFAULT_CHUNK_SIZE);
8331 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8332 let config = TorrentConfig {
8333 listen_port: 0,
8334 ..Default::default()
8335 };
8336 let (atx, amask) = test_alert_channel();
8337 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8338 TorrentHandle::from_torrent(
8339 meta,
8340 irontide_core::TorrentVersion::V1Only,
8341 None,
8342 dh,
8343 dm,
8344 config,
8345 test_dht_rx(),
8346 test_dht_rx(),
8347 None,
8348 None,
8349 crate::slot_tuner::SlotTuner::disabled(4),
8350 atx,
8351 amask,
8352 None,
8353 None,
8354 test_ban_manager(),
8355 test_ip_filter(),
8356 Arc::new(Vec::new()),
8357 None,
8358 None,
8359 Arc::new(crate::transport::NetworkFactory::tokio()),
8360 None,
8361 Arc::new(crate::stats::SessionCounters::new()),
8362 )
8363 .await
8364 .unwrap()
8365 }
8366
8367 #[tokio::test]
8373 async fn set_file_priority_updates_wanted_and_priorities() {
8374 let handle = spawn_test_torrent_multifile().await;
8375
8376 let prios = handle.file_priorities().await.unwrap();
8377 assert_eq!(prios.len(), 3);
8378 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
8379
8380 handle
8381 .set_file_priority(1, FilePriority::Skip)
8382 .await
8383 .unwrap();
8384 assert_eq!(
8385 handle.file_priorities().await.unwrap()[1],
8386 FilePriority::Skip
8387 );
8388
8389 handle
8390 .set_file_priority(1, FilePriority::Normal)
8391 .await
8392 .unwrap();
8393 assert_eq!(
8394 handle.file_priorities().await.unwrap()[1],
8395 FilePriority::Normal
8396 );
8397
8398 assert!(
8400 handle
8401 .set_file_priority(99, FilePriority::High)
8402 .await
8403 .is_err()
8404 );
8405
8406 handle.shutdown().await.unwrap();
8407 tokio::time::sleep(Duration::from_millis(50)).await;
8408 }
8409
8410 fn priority_test_actor(files: &[(u64, &str)], piece_length: u64) -> TorrentActor {
8416 use irontide_storage::Bitfield;
8417 let meta = make_multi_file_meta(files, piece_length);
8418 let total: u64 = files.iter().map(|(l, _)| *l).sum();
8419 let lengths = Lengths::new(total, piece_length, DEFAULT_CHUNK_SIZE);
8420 let num_pieces = lengths.num_pieces();
8421 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8422
8423 let mut actor = TorrentActor::for_throttle_test(num_pieces, 0);
8424 actor.file_priorities = vec![FilePriority::Normal; files.len()];
8425 actor.wanted_pieces = crate::piece_selector::build_wanted_pieces(
8426 &actor.file_priorities,
8427 &file_lengths,
8428 &lengths,
8429 );
8430 actor.cached_files = Some(build_cached_file_info(&meta, &lengths));
8431
8432 let we_have = Bitfield::new(num_pieces);
8433 actor.atomic_states = Some(Arc::new(crate::piece_reservation::AtomicPieceStates::new(
8434 num_pieces,
8435 &we_have,
8436 &actor.wanted_pieces,
8437 )));
8438 actor.piece_tracker = Some(crate::piece_reservation::PieceTracker::new(
8439 num_pieces,
8440 &we_have,
8441 &actor.wanted_pieces,
8442 ));
8443 actor.meta = Some(meta);
8444 actor.lengths = Some(lengths);
8445 actor
8446 }
8447
8448 #[tokio::test]
8453 async fn apply_file_priority_scoped_matches_full_rebuild() {
8454 let files: &[(u64, &str)] = &[(100, "a"), (150, "b"), (100, "c"), (250, "d")];
8455 let piece_length = 100;
8456 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8457 for skip_idx in 0..files.len() {
8458 let mut actor = priority_test_actor(files, piece_length);
8459 actor
8460 .apply_file_priority_scoped(skip_idx, FilePriority::Skip)
8461 .unwrap();
8462
8463 let mut ref_prios = vec![FilePriority::Normal; files.len()];
8464 ref_prios[skip_idx] = FilePriority::Skip;
8465 let reference = crate::piece_selector::build_wanted_pieces(
8466 &ref_prios,
8467 &file_lengths,
8468 actor.lengths.as_ref().unwrap(),
8469 );
8470 for p in 0..actor.num_pieces {
8471 assert_eq!(
8472 actor.wanted_pieces.get(p),
8473 reference.get(p),
8474 "piece {p} mismatch after scoped skip of file {skip_idx}"
8475 );
8476 }
8477 }
8478 }
8479
8480 #[tokio::test]
8484 async fn apply_file_priority_scoped_handles_sub_piece_files() {
8485 let files: &[(u64, &str)] = &[(30, "a"), (30, "b"), (40, "c"), (100, "d")];
8486 let mut actor = priority_test_actor(files, 100);
8487
8488 actor
8489 .apply_file_priority_scoped(0, FilePriority::Skip)
8490 .unwrap();
8491 assert!(
8492 actor.wanted_pieces.get(0),
8493 "piece 0 wanted: b,c still want it"
8494 );
8495 actor
8496 .apply_file_priority_scoped(1, FilePriority::Skip)
8497 .unwrap();
8498 actor
8499 .apply_file_priority_scoped(2, FilePriority::Skip)
8500 .unwrap();
8501 assert!(
8502 !actor.wanted_pieces.get(0),
8503 "piece 0 unwanted: a,b,c all skip"
8504 );
8505 assert!(
8506 actor.wanted_pieces.get(1),
8507 "piece 1 still wanted (d Normal)"
8508 );
8509
8510 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8511 let prios = vec![
8512 FilePriority::Skip,
8513 FilePriority::Skip,
8514 FilePriority::Skip,
8515 FilePriority::Normal,
8516 ];
8517 let reference = crate::piece_selector::build_wanted_pieces(
8518 &prios,
8519 &file_lengths,
8520 actor.lengths.as_ref().unwrap(),
8521 );
8522 for p in 0..actor.num_pieces {
8523 assert_eq!(
8524 actor.wanted_pieces.get(p),
8525 reference.get(p),
8526 "piece {p} mismatch"
8527 );
8528 }
8529 }
8530
8531 #[tokio::test]
8534 async fn apply_file_priority_scoped_zero_length_file_no_panic() {
8535 let files: &[(u64, &str)] = &[(100, "a"), (0, "empty"), (100, "c")];
8536 let mut actor = priority_test_actor(files, 100);
8537
8538 let r = actor
8539 .apply_file_priority_scoped(1, FilePriority::Skip)
8540 .unwrap();
8541 assert!(
8542 r.0 > r.1,
8543 "zero-length file yields an empty range, got {r:?}"
8544 );
8545
8546 actor
8547 .apply_file_priority_scoped(0, FilePriority::Skip)
8548 .unwrap();
8549
8550 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8551 let prios = vec![FilePriority::Skip, FilePriority::Skip, FilePriority::Normal];
8552 let reference = crate::piece_selector::build_wanted_pieces(
8553 &prios,
8554 &file_lengths,
8555 actor.lengths.as_ref().unwrap(),
8556 );
8557 for p in 0..actor.num_pieces {
8558 assert_eq!(
8559 actor.wanted_pieces.get(p),
8560 reference.get(p),
8561 "piece {p} mismatch"
8562 );
8563 }
8564 }
8565
8566 #[tokio::test]
8569 async fn sync_piece_states_for_range_only_touches_range() {
8570 let files: &[(u64, &str)] = &[(200, "a"), (200, "b"), (200, "c")];
8571 let mut actor = priority_test_actor(files, 100); let (first, last) = actor
8573 .apply_file_priority_scoped(1, FilePriority::Skip)
8574 .unwrap();
8575 assert_eq!((first, last), (2, 3));
8576 actor.sync_piece_states_for_range(first, last);
8577
8578 let atomic = actor.atomic_states.as_ref().unwrap();
8579 assert_eq!(
8580 atomic.get(2),
8581 crate::piece_reservation::PieceState::Unwanted
8582 );
8583 assert_eq!(
8584 atomic.get(3),
8585 crate::piece_reservation::PieceState::Unwanted
8586 );
8587 for p in [0u32, 1, 4, 5] {
8588 assert_eq!(
8589 atomic.get(p),
8590 crate::piece_reservation::PieceState::Available,
8591 "piece {p} outside the range must be untouched"
8592 );
8593 }
8594 }
8595
8596 #[tokio::test]
8602 async fn order_map_coalesces_and_gen_is_monotone() {
8603 let mut actor = priority_test_actor(&[(200, "a"), (200, "b"), (200, "c")], 100);
8605 let gen0 = actor.order_map_tx.borrow().generation;
8606
8607 actor
8610 .apply_file_priority_scoped(0, FilePriority::Skip)
8611 .unwrap();
8612 actor.order_map_dirty = true;
8613 actor
8614 .apply_file_priority_scoped(2, FilePriority::Skip)
8615 .unwrap();
8616 actor.order_map_dirty = true;
8617 assert_eq!(
8618 actor.order_map_tx.borrow().generation,
8619 gen0,
8620 "no order-map rebuild before the tick"
8621 );
8622
8623 actor.rebuild_order_map_now();
8625 assert_eq!(
8626 actor.order_map_tx.borrow().generation,
8627 gen0 + 1,
8628 "exactly one coalesced rebuild"
8629 );
8630 assert!(!actor.order_map_dirty, "dirty flag cleared after rebuild");
8631
8632 let map = actor.order_map_tx.borrow();
8635 for p in [0u32, 1, 4, 5] {
8636 assert!(
8637 !map.order.contains(&p),
8638 "skipped piece {p} must be absent from the order"
8639 );
8640 }
8641 for p in [2u32, 3] {
8642 assert!(
8643 map.order.contains(&p),
8644 "wanted piece {p} must be present in the order"
8645 );
8646 }
8647 }
8648
8649 #[tokio::test]
8653 async fn rebuild_order_map_now_clears_dirty_flag() {
8654 let mut actor = priority_test_actor(&[(200, "a"), (200, "b")], 100);
8655 actor.order_map_dirty = true;
8656 actor.rebuild_order_map_now();
8657 assert!(!actor.order_map_dirty);
8658 }
8659
8660 #[tokio::test]
8661 async fn resume_data_preserves_file_priorities() {
8662 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
8663 let mut torrent_bytes = b"d4:info".to_vec();
8664 torrent_bytes.extend_from_slice(info_bytes);
8665 torrent_bytes.push(b'e');
8666
8667 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
8668 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
8669 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8670 let config = TorrentConfig {
8671 listen_port: 0,
8672 ..Default::default()
8673 };
8674
8675 let (atx, amask) = test_alert_channel();
8676 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8677 let handle = TorrentHandle::from_torrent(
8678 meta,
8679 irontide_core::TorrentVersion::V1Only,
8680 None,
8681 dh,
8682 dm,
8683 config,
8684 test_dht_rx(),
8685 test_dht_rx(),
8686 None,
8687 None,
8688 crate::slot_tuner::SlotTuner::disabled(4),
8689 atx,
8690 amask,
8691 None,
8692 None,
8693 test_ban_manager(),
8694 test_ip_filter(),
8695 Arc::new(Vec::new()),
8696 None,
8697 None,
8698 Arc::new(crate::transport::NetworkFactory::tokio()),
8699 None, Arc::new(crate::stats::SessionCounters::new()),
8701 )
8702 .await
8703 .unwrap();
8704
8705 handle
8707 .set_file_priority(0, FilePriority::High)
8708 .await
8709 .unwrap();
8710 handle
8711 .set_file_priority(1, FilePriority::Skip)
8712 .await
8713 .unwrap();
8714
8715 let rd = handle.save_resume_data().await.unwrap();
8717 assert_eq!(rd.file_priority, vec![7, 0]); let encoded = irontide_bencode::to_bytes(&rd).unwrap();
8721 let decoded: irontide_core::FastResumeData =
8722 irontide_bencode::from_bytes(&encoded).unwrap();
8723 assert_eq!(decoded.file_priority, vec![7, 0]);
8724
8725 handle.shutdown().await.unwrap();
8726 tokio::time::sleep(Duration::from_millis(50)).await;
8727 }
8728
8729 #[tokio::test]
8732 async fn upload_rate_limiting_caps_throughput() {
8733 let data = vec![0xAB; 16384]; let meta = make_test_torrent(&data, 16384);
8739 let info_hash = meta.info_hash;
8740 let storage = make_seeded_storage(&data, 16384);
8741
8742 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8743 let listen_addr = listener.local_addr().unwrap();
8744
8745 let config = TorrentConfig {
8746 listen_port: listen_addr.port(),
8747 upload_rate_limit: 1024, ..test_config()
8749 };
8750
8751 drop(listener);
8752 let (atx, amask) = test_alert_channel();
8753 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8754 let handle = TorrentHandle::from_torrent(
8755 meta,
8756 irontide_core::TorrentVersion::V1Only,
8757 None,
8758 dh,
8759 dm,
8760 config,
8761 test_dht_rx(),
8762 test_dht_rx(),
8763 None,
8764 None,
8765 crate::slot_tuner::SlotTuner::disabled(4),
8766 atx,
8767 amask,
8768 None,
8769 None,
8770 test_ban_manager(),
8771 test_ip_filter(),
8772 Arc::new(Vec::new()),
8773 None,
8774 None,
8775 Arc::new(crate::transport::NetworkFactory::tokio()),
8776 None, Arc::new(crate::stats::SessionCounters::new()),
8778 )
8779 .await
8780 .unwrap();
8781
8782 tokio::time::sleep(Duration::from_millis(50)).await;
8783
8784 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8786 let (reader, writer) = tokio::io::split(stream);
8787 let mut writer = writer;
8788 let mut reader = reader;
8789
8790 let hs = Handshake::new(
8791 info_hash,
8792 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
8793 );
8794 writer.write_all(&hs.to_bytes()).await.unwrap();
8795 writer.flush().await.unwrap();
8796 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8797 reader.read_exact(&mut hs_buf).await.unwrap();
8798
8799 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8800 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8801
8802 let _msg = framed_read.next().await;
8804 let ext_hs = ExtHandshake::new();
8805 let payload = ext_hs.to_bytes().unwrap();
8806 framed_write
8807 .send(Message::Extended { ext_id: 0, payload })
8808 .await
8809 .unwrap();
8810
8811 let _bf_msg = framed_read.next().await;
8813
8814 framed_write.send(Message::Interested).await.unwrap();
8816
8817 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
8819 loop {
8820 tokio::select! {
8821 msg = framed_read.next() => {
8822 match msg {
8823 Some(Ok(Message::Unchoke)) => break,
8824 Some(Ok(_)) => {}
8825 _ => panic!("connection closed before unchoke"),
8826 }
8827 }
8828 () = tokio::time::sleep_until(deadline) => {
8829 panic!("timed out waiting for unchoke");
8830 }
8831 }
8832 }
8833
8834 framed_write
8836 .send(Message::Request {
8837 index: 0,
8838 begin: 0,
8839 length: 16384,
8840 })
8841 .await
8842 .unwrap();
8843
8844 let mut got_piece = false;
8848 if let Ok(true) = tokio::time::timeout(Duration::from_secs(2), async {
8849 loop {
8850 match framed_read.next().await {
8851 Some(Ok(Message::Piece { .. })) => return true,
8852 Some(Ok(_)) => {}
8853 _ => return false,
8854 }
8855 }
8856 })
8857 .await
8858 {
8859 got_piece = true;
8860 }
8861
8862 assert!(
8864 !got_piece,
8865 "piece should be delayed by rate limiter (1 KB/s for 16 KB chunk)"
8866 );
8867
8868 let stats = handle.stats().await.unwrap();
8870 assert_eq!(stats.uploaded, 0); handle.shutdown().await.unwrap();
8873 }
8874
8875 #[tokio::test]
8876 async fn unlimited_rate_has_no_effect() {
8877 let data = vec![0xAB; 32768];
8879 let meta = make_test_torrent(&data, 16384);
8880 let storage = make_storage(&data, 16384);
8881 let config = test_config();
8882
8883 assert_eq!(config.upload_rate_limit, 0);
8885 assert_eq!(config.download_rate_limit, 0);
8886
8887 let (atx, amask) = test_alert_channel();
8888 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8889 let handle = TorrentHandle::from_torrent(
8890 meta,
8891 irontide_core::TorrentVersion::V1Only,
8892 None,
8893 dh,
8894 dm,
8895 config,
8896 test_dht_rx(),
8897 test_dht_rx(),
8898 None,
8899 None,
8900 crate::slot_tuner::SlotTuner::disabled(4),
8901 atx,
8902 amask,
8903 None,
8904 None,
8905 test_ban_manager(),
8906 test_ip_filter(),
8907 Arc::new(Vec::new()),
8908 None,
8909 None,
8910 Arc::new(crate::transport::NetworkFactory::tokio()),
8911 None, Arc::new(crate::stats::SessionCounters::new()),
8913 )
8914 .await
8915 .unwrap();
8916
8917 let stats = handle.stats().await.unwrap();
8918 assert_eq!(stats.state, TorrentState::Downloading);
8919 assert_eq!(stats.pieces_total, 2);
8920
8921 handle.shutdown().await.unwrap();
8922 }
8923
8924 #[tokio::test]
8925 async fn download_rate_limiting_throttles_requests() {
8926 let data = vec![0xAB; 32768];
8929 let meta = make_test_torrent(&data, 16384);
8930 let info_hash = meta.info_hash;
8931 let storage = make_storage(&data, 16384);
8932
8933 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8934 let listen_addr = listener.local_addr().unwrap();
8935
8936 let config = TorrentConfig {
8937 listen_port: listen_addr.port(),
8938 download_rate_limit: 1024, ..test_config()
8940 };
8941
8942 drop(listener);
8943 let (atx, amask) = test_alert_channel();
8944 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8945 let handle = TorrentHandle::from_torrent(
8946 meta,
8947 irontide_core::TorrentVersion::V1Only,
8948 None,
8949 dh,
8950 dm,
8951 config,
8952 test_dht_rx(),
8953 test_dht_rx(),
8954 None,
8955 None,
8956 crate::slot_tuner::SlotTuner::disabled(4),
8957 atx,
8958 amask,
8959 None,
8960 None,
8961 test_ban_manager(),
8962 test_ip_filter(),
8963 Arc::new(Vec::new()),
8964 None,
8965 None,
8966 Arc::new(crate::transport::NetworkFactory::tokio()),
8967 None, Arc::new(crate::stats::SessionCounters::new()),
8969 )
8970 .await
8971 .unwrap();
8972
8973 tokio::time::sleep(Duration::from_millis(50)).await;
8974
8975 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8977 let (reader, writer) = tokio::io::split(stream);
8978 let mut writer = writer;
8979 let mut reader = reader;
8980
8981 let hs = Handshake::new(
8982 info_hash,
8983 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
8984 );
8985 writer.write_all(&hs.to_bytes()).await.unwrap();
8986 writer.flush().await.unwrap();
8987 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8988 reader.read_exact(&mut hs_buf).await.unwrap();
8989
8990 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8991 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8992
8993 let _msg = framed_read.next().await;
8995 let ext_hs = ExtHandshake::new();
8996 let payload = ext_hs.to_bytes().unwrap();
8997 framed_write
8998 .send(Message::Extended { ext_id: 0, payload })
8999 .await
9000 .unwrap();
9001
9002 let mut bf = Bitfield::new(2);
9004 bf.set(0);
9005 bf.set(1);
9006 framed_write
9007 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
9008 .await
9009 .unwrap();
9010
9011 framed_write.send(Message::Unchoke).await.unwrap();
9013
9014 let mut requests_received = 0u32;
9018 let deadline = tokio::time::Instant::now() + Duration::from_millis(500);
9019 loop {
9020 match tokio::time::timeout(
9021 deadline.saturating_duration_since(tokio::time::Instant::now()),
9022 framed_read.next(),
9023 )
9024 .await
9025 {
9026 Ok(Some(Ok(Message::Request { .. }))) => {
9027 requests_received += 1;
9028 }
9029 Ok(Some(Ok(_))) => {}
9030 _ => break,
9031 }
9032 }
9033
9034 let stats = handle.stats().await.unwrap();
9035 assert_eq!(stats.state, TorrentState::Downloading);
9036
9037 assert!(
9040 requests_received <= 2,
9041 "with 1 KB/s limit, should get very few requests, got {requests_received}"
9042 );
9043
9044 handle.shutdown().await.unwrap();
9045 }
9046
9047 #[test]
9050 fn piece_contributor_tracking() {
9051 use std::net::IpAddr;
9052 let mut contributors: HashMap<u32, HashSet<IpAddr>> = HashMap::new();
9053 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9054 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9055
9056 contributors.entry(0).or_default().insert(ip1);
9057 contributors.entry(0).or_default().insert(ip2);
9058 assert_eq!(contributors[&0].len(), 2);
9059 assert!(contributors[&0].contains(&ip1));
9060 assert!(contributors[&0].contains(&ip2));
9061
9062 contributors.remove(&0);
9064 assert!(!contributors.contains_key(&0));
9065 }
9066
9067 #[test]
9068 fn parole_enter_on_hash_failure() {
9069 use crate::ban::ParoleState;
9070 use std::net::IpAddr;
9071
9072 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9073 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9074 let contributors = vec![ip1, ip2];
9075
9076 let parole = ParoleState {
9078 original_contributors: contributors.into_iter().collect(),
9079 parole_peer: None,
9080 };
9081
9082 assert_eq!(parole.original_contributors.len(), 2);
9083 assert!(parole.original_contributors.contains(&ip1));
9084 assert!(parole.original_contributors.contains(&ip2));
9085 assert!(parole.parole_peer.is_none());
9086 }
9087
9088 #[test]
9089 fn parole_success_strikes_originals() {
9090 use crate::ban::{BanConfig, BanManager, ParoleState};
9091 use std::net::IpAddr;
9092
9093 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9094 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9095 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
9096
9097 let mut mgr = BanManager::new(BanConfig {
9098 max_failures: 2,
9099 use_parole: true,
9100 });
9101
9102 let parole = ParoleState {
9103 original_contributors: [ip1, ip2].into_iter().collect(),
9104 parole_peer: Some(parole_ip),
9105 };
9106
9107 for ip in &parole.original_contributors {
9109 mgr.record_strike(*ip);
9110 }
9111
9112 assert_eq!(*mgr.strikes_map().get(&ip1).unwrap(), 1);
9113 assert_eq!(*mgr.strikes_map().get(&ip2).unwrap(), 1);
9114 assert!(!mgr.strikes_map().contains_key(&parole_ip));
9116
9117 for ip in &parole.original_contributors {
9119 mgr.record_strike(*ip);
9120 }
9121 assert!(mgr.is_banned(&ip1));
9122 assert!(mgr.is_banned(&ip2));
9123 }
9124
9125 #[test]
9126 fn parole_failure_strikes_parole_peer() {
9127 use crate::ban::{BanConfig, BanManager, ParoleState};
9128 use std::net::IpAddr;
9129
9130 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9131 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
9132
9133 let mut mgr = BanManager::new(BanConfig {
9134 max_failures: 2,
9135 use_parole: true,
9136 });
9137
9138 let parole = ParoleState {
9139 original_contributors: [ip1].into_iter().collect(),
9140 parole_peer: Some(parole_ip),
9141 };
9142
9143 if let Some(pp) = parole.parole_peer {
9145 mgr.record_strike(pp);
9146 }
9147
9148 assert_eq!(*mgr.strikes_map().get(&parole_ip).unwrap(), 1);
9149 assert!(!mgr.strikes_map().contains_key(&ip1));
9150 }
9151
9152 #[tokio::test]
9153 async fn banned_peer_rejected_on_connect() {
9154 let data = vec![0xAB; 32768];
9155 let meta = make_test_torrent(&data, 16384);
9156 let storage = make_storage(&data, 16384);
9157 let config = test_config();
9158 let ban_mgr = test_ban_manager();
9159
9160 let banned_ip: std::net::IpAddr = "192.168.1.100".parse().unwrap();
9162 ban_mgr.write().ban(banned_ip);
9163
9164 let (atx, amask) = test_alert_channel();
9165 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9166 let handle = TorrentHandle::from_torrent(
9167 meta,
9168 irontide_core::TorrentVersion::V1Only,
9169 None,
9170 dh,
9171 dm,
9172 config,
9173 test_dht_rx(),
9174 test_dht_rx(),
9175 None,
9176 None,
9177 crate::slot_tuner::SlotTuner::disabled(4),
9178 atx,
9179 amask,
9180 None,
9181 None,
9182 Arc::clone(&ban_mgr),
9183 test_ip_filter(),
9184 Arc::new(Vec::new()),
9185 None,
9186 None,
9187 Arc::new(crate::transport::NetworkFactory::tokio()),
9188 None, Arc::new(crate::stats::SessionCounters::new()),
9190 )
9191 .await
9192 .unwrap();
9193
9194 handle
9196 .add_peers(
9197 vec![
9198 SocketAddr::new(banned_ip, 6881),
9199 "10.0.0.1:6881".parse().unwrap(),
9200 ],
9201 PeerSource::Tracker,
9202 )
9203 .await
9204 .unwrap();
9205
9206 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9207 let stats = handle.stats().await.unwrap();
9208 assert!(
9211 stats.peers_available + stats.peers_connected <= 1,
9212 "banned peer should not be added: available={}, connected={}",
9213 stats.peers_available,
9214 stats.peers_connected
9215 );
9216
9217 handle.shutdown().await.unwrap();
9218 }
9219
9220 #[test]
9221 fn banned_peer_filtered_from_available() {
9222 use crate::ban::{BanConfig, BanManager};
9223 use std::net::IpAddr;
9224
9225 let banned_ip: IpAddr = "192.168.1.200".parse().unwrap();
9226 let ok_ip: IpAddr = "10.0.0.1".parse().unwrap();
9227
9228 let mgr = BanManager::new(BanConfig::default());
9229 assert!(!mgr.is_banned(&banned_ip));
9231 assert!(!mgr.is_banned(&ok_ip));
9232
9233 let mut mgr = BanManager::new(BanConfig::default());
9234 mgr.ban(banned_ip);
9235
9236 assert!(mgr.is_banned(&banned_ip));
9238 assert!(!mgr.is_banned(&ok_ip));
9239 }
9240
9241 #[test]
9244 fn hashing_threads_config_default() {
9245 let s = irontide_settings::Settings::default();
9246 let expected = {
9247 let cores = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
9248 (cores / 4).clamp(2, 8)
9249 };
9250 assert_eq!(s.hashing_threads, expected);
9251 let tc = TorrentConfig::default();
9252 assert_eq!(tc.hashing_threads, expected);
9253 }
9254
9255 #[tokio::test]
9256 async fn checking_state_and_progress_alerts() {
9257 use crate::alert::AlertKind;
9258
9259 let data = vec![0xEEu8; 65536]; let meta = make_test_torrent(&data, 16384);
9261 let storage = make_seeded_storage(&data, 16384);
9262 let config = test_config();
9263
9264 let (atx, amask) = test_alert_channel();
9265 let mut rx = atx.subscribe();
9266 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9267 let handle = TorrentHandle::from_torrent(
9268 meta,
9269 irontide_core::TorrentVersion::V1Only,
9270 None,
9271 dh,
9272 dm,
9273 config,
9274 test_dht_rx(),
9275 test_dht_rx(),
9276 None,
9277 None,
9278 crate::slot_tuner::SlotTuner::disabled(4),
9279 atx,
9280 amask,
9281 None,
9282 None,
9283 test_ban_manager(),
9284 test_ip_filter(),
9285 Arc::new(Vec::new()),
9286 None,
9287 None,
9288 Arc::new(crate::transport::NetworkFactory::tokio()),
9289 None, Arc::new(crate::stats::SessionCounters::new()),
9291 )
9292 .await
9293 .unwrap();
9294
9295 let mut saw_checking = false;
9297 let mut progress_values: Vec<f32> = Vec::new();
9298 let mut saw_checked = false;
9299 let mut checked_have = 0u32;
9300 let mut checked_total = 0u32;
9301
9302 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
9303 while tokio::time::Instant::now() < deadline {
9304 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
9305 Ok(Ok(alert)) => match alert.kind {
9306 AlertKind::StateChanged {
9307 new_state: TorrentState::Checking,
9308 ..
9309 } => {
9310 saw_checking = true;
9311 }
9312 AlertKind::CheckingProgress { progress, .. } => {
9313 progress_values.push(progress);
9314 }
9315 AlertKind::TorrentChecked {
9316 pieces_have,
9317 pieces_total,
9318 ..
9319 } => {
9320 saw_checked = true;
9321 checked_have = pieces_have;
9322 checked_total = pieces_total;
9323 break;
9324 }
9325 _ => {}
9326 },
9327 _ => break,
9328 }
9329 }
9330
9331 assert!(saw_checking, "should have seen StateChanged → Checking");
9332 assert!(
9333 !progress_values.is_empty(),
9334 "should have seen CheckingProgress alerts"
9335 );
9336 for w in progress_values.windows(2) {
9338 assert!(
9339 w[1] >= w[0],
9340 "progress should be monotonically increasing: {} < {}",
9341 w[0],
9342 w[1]
9343 );
9344 }
9345 assert!(saw_checked, "should have seen TorrentChecked");
9346 assert_eq!(checked_have, 4);
9347 assert_eq!(checked_total, 4);
9348
9349 tokio::time::sleep(Duration::from_millis(50)).await;
9351 let stats = handle.stats().await.unwrap();
9352 assert_eq!(stats.state, TorrentState::Seeding);
9353
9354 handle.shutdown().await.unwrap();
9355 }
9356
9357 #[tokio::test]
9358 #[allow(clippy::float_cmp, reason = "exact sentinel value comparison (0.0)")]
9359 async fn checking_progress_in_stats() {
9360 let data = vec![0xAB; 32768];
9362 let meta = make_test_torrent(&data, 16384);
9363 let storage = make_storage(&data, 16384);
9364 let config = test_config();
9365
9366 let (atx, amask) = test_alert_channel();
9367 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9368 let handle = TorrentHandle::from_torrent(
9369 meta,
9370 irontide_core::TorrentVersion::V1Only,
9371 None,
9372 dh,
9373 dm,
9374 config,
9375 test_dht_rx(),
9376 test_dht_rx(),
9377 None,
9378 None,
9379 crate::slot_tuner::SlotTuner::disabled(4),
9380 atx,
9381 amask,
9382 None,
9383 None,
9384 test_ban_manager(),
9385 test_ip_filter(),
9386 Arc::new(Vec::new()),
9387 None,
9388 None,
9389 Arc::new(crate::transport::NetworkFactory::tokio()),
9390 None, Arc::new(crate::stats::SessionCounters::new()),
9392 )
9393 .await
9394 .unwrap();
9395
9396 tokio::time::sleep(Duration::from_millis(100)).await;
9398
9399 let stats = handle.stats().await.unwrap();
9400 assert_eq!(stats.state, TorrentState::Downloading);
9401 assert_eq!(
9402 stats.checking_progress, 0.0,
9403 "checking_progress should be 0.0 when not checking"
9404 );
9405
9406 handle.shutdown().await.unwrap();
9407 }
9408
9409 #[tokio::test]
9410 async fn verify_pieces_partial_data() {
9411 use crate::alert::AlertKind;
9412
9413 let data = vec![0xCCu8; 65536]; let meta = make_test_torrent(&data, 16384);
9416
9417 let lengths = Lengths::new(data.len() as u64, 16384, DEFAULT_CHUNK_SIZE);
9419 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
9420 for p in 0..2u32 {
9421 let offset = lengths.piece_offset(p) as usize;
9422 let size = lengths.piece_size(p) as usize;
9423 storage
9424 .write_chunk(p, 0, &data[offset..offset + size])
9425 .unwrap();
9426 }
9427 let config = test_config();
9430 let (atx, amask) = test_alert_channel();
9431 let mut rx = atx.subscribe();
9432 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9433 let handle = TorrentHandle::from_torrent(
9434 meta,
9435 irontide_core::TorrentVersion::V1Only,
9436 None,
9437 dh,
9438 dm,
9439 config,
9440 test_dht_rx(),
9441 test_dht_rx(),
9442 None,
9443 None,
9444 crate::slot_tuner::SlotTuner::disabled(4),
9445 atx,
9446 amask,
9447 None,
9448 None,
9449 test_ban_manager(),
9450 test_ip_filter(),
9451 Arc::new(Vec::new()),
9452 None,
9453 None,
9454 Arc::new(crate::transport::NetworkFactory::tokio()),
9455 None, Arc::new(crate::stats::SessionCounters::new()),
9457 )
9458 .await
9459 .unwrap();
9460
9461 let mut checked_have = 0u32;
9463 let mut checked_total = 0u32;
9464 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
9465 while tokio::time::Instant::now() < deadline {
9466 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
9467 Ok(Ok(alert)) => {
9468 if let AlertKind::TorrentChecked {
9469 pieces_have,
9470 pieces_total,
9471 ..
9472 } = alert.kind
9473 {
9474 checked_have = pieces_have;
9475 checked_total = pieces_total;
9476 break;
9477 }
9478 }
9479 _ => break,
9480 }
9481 }
9482
9483 assert_eq!(checked_have, 2, "only 2 pieces should be valid");
9484 assert_eq!(checked_total, 4);
9485
9486 tokio::time::sleep(Duration::from_millis(50)).await;
9488 let stats = handle.stats().await.unwrap();
9489 assert_eq!(stats.state, TorrentState::Downloading);
9490 assert_eq!(stats.pieces_have, 2);
9491 assert_eq!(stats.pieces_total, 4);
9492
9493 handle.shutdown().await.unwrap();
9494 }
9495
9496 #[tokio::test]
9499 async fn ip_filter_blocks_peers_in_handle_add_peers() {
9500 let data = vec![0xCD; 32768];
9501 let meta = make_test_torrent(&data, 16384);
9502 let storage = make_storage(&data, 16384);
9503 let config = test_config();
9504
9505 let ip_filter = {
9507 let mut f = crate::ip_filter::IpFilter::new();
9508 f.add_rule(
9509 "203.0.113.0".parse().unwrap(),
9510 "203.0.113.255".parse().unwrap(),
9511 1,
9512 );
9513 Arc::new(parking_lot::RwLock::new(f))
9514 };
9515
9516 let (atx, amask) = test_alert_channel();
9517 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9518 let handle = TorrentHandle::from_torrent(
9519 meta,
9520 irontide_core::TorrentVersion::V1Only,
9521 None,
9522 dh,
9523 dm,
9524 config,
9525 test_dht_rx(),
9526 test_dht_rx(),
9527 None,
9528 None,
9529 crate::slot_tuner::SlotTuner::disabled(4),
9530 atx,
9531 amask,
9532 None,
9533 None,
9534 test_ban_manager(),
9535 Arc::clone(&ip_filter),
9536 Arc::new(Vec::new()),
9537 None,
9538 None,
9539 Arc::new(crate::transport::NetworkFactory::tokio()),
9540 None, Arc::new(crate::stats::SessionCounters::new()),
9542 )
9543 .await
9544 .unwrap();
9545
9546 let blocked_addr: SocketAddr = "203.0.113.42:6881".parse().unwrap();
9548 let allowed_addr: SocketAddr = "198.51.100.1:6881".parse().unwrap();
9549 handle
9550 .add_peers(vec![blocked_addr, allowed_addr], PeerSource::Tracker)
9551 .await
9552 .unwrap();
9553
9554 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9555 let stats = handle.stats().await.unwrap();
9556 assert!(
9558 stats.peers_available + stats.peers_connected <= 1,
9559 "blocked peer should not be added: available={}, connected={}",
9560 stats.peers_available,
9561 stats.peers_connected
9562 );
9563
9564 handle.shutdown().await.unwrap();
9565 }
9566
9567 #[tokio::test]
9568 async fn set_ip_filter_replaces_filter_and_blocks_new_ip() {
9569 let data = vec![0xCD; 32768];
9572 let meta = make_test_torrent(&data, 16384);
9573 let storage = make_storage(&data, 16384);
9574 let config = test_config();
9575
9576 let ip_filter: irontide_session_types::SharedIpFilter =
9578 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
9579
9580 let (atx, amask) = test_alert_channel();
9581 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9582 let handle = TorrentHandle::from_torrent(
9583 meta,
9584 irontide_core::TorrentVersion::V1Only,
9585 None,
9586 dh,
9587 dm,
9588 config,
9589 test_dht_rx(),
9590 test_dht_rx(),
9591 None,
9592 None,
9593 crate::slot_tuner::SlotTuner::disabled(4),
9594 atx,
9595 amask,
9596 None,
9597 None,
9598 test_ban_manager(),
9599 Arc::clone(&ip_filter),
9600 Arc::new(Vec::new()),
9601 None,
9602 None,
9603 Arc::new(crate::transport::NetworkFactory::tokio()),
9604 None, Arc::new(crate::stats::SessionCounters::new()),
9606 )
9607 .await
9608 .unwrap();
9609
9610 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9613 let local_addr = listener.local_addr().unwrap();
9614 handle
9615 .add_peers(vec![local_addr], PeerSource::Tracker)
9616 .await
9617 .unwrap();
9618 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9619 let stats = handle.stats().await.unwrap();
9620 assert!(
9621 stats.peers_available + stats.peers_connected >= 1,
9622 "peer should be allowed initially"
9623 );
9624 handle.shutdown().await.unwrap();
9625
9626 {
9628 let mut f = ip_filter.write();
9629 f.add_rule(
9630 "198.51.100.0".parse().unwrap(),
9631 "198.51.100.255".parse().unwrap(),
9632 1,
9633 );
9634 }
9635
9636 assert!(ip_filter.read().is_blocked("198.51.100.1".parse().unwrap()));
9638 assert!(!ip_filter.read().is_blocked("203.0.113.1".parse().unwrap()));
9640 }
9641
9642 #[test]
9643 fn relocate_files_moves_and_cleans_up() {
9644 let tmp = std::env::temp_dir().join(format!("torrent_relocate_{}", std::process::id()));
9645 let src = tmp.join("src");
9646 let dst = tmp.join("dst");
9647
9648 let subdir = src.join("TorrentName").join("subdir");
9652 std::fs::create_dir_all(&subdir).unwrap();
9653 std::fs::write(subdir.join("file1.txt"), b"hello").unwrap();
9654 std::fs::write(src.join("TorrentName").join("file2.txt"), b"world").unwrap();
9655
9656 let file_paths = vec![
9657 std::path::PathBuf::from("TorrentName/subdir/file1.txt"),
9658 std::path::PathBuf::from("TorrentName/file2.txt"),
9659 ];
9660
9661 relocate_files(&src, &dst, &file_paths).unwrap();
9662
9663 assert_eq!(
9665 std::fs::read_to_string(dst.join("TorrentName/subdir/file1.txt")).unwrap(),
9666 "hello"
9667 );
9668 assert_eq!(
9669 std::fs::read_to_string(dst.join("TorrentName/file2.txt")).unwrap(),
9670 "world"
9671 );
9672
9673 assert!(!src.join("TorrentName").join("subdir").exists());
9675 assert!(!src.join("TorrentName").exists());
9676
9677 let _ = std::fs::remove_dir_all(&tmp);
9679 }
9680
9681 #[test]
9682 fn relocate_files_skips_missing() {
9683 let tmp =
9684 std::env::temp_dir().join(format!("torrent_relocate_skip_{}", std::process::id()));
9685 let src = tmp.join("src");
9686 let dst = tmp.join("dst");
9687 std::fs::create_dir_all(&src).unwrap();
9688
9689 let file_paths = vec![std::path::PathBuf::from("nonexistent.txt")];
9691 relocate_files(&src, &dst, &file_paths).unwrap();
9692
9693 assert!(!dst.join("nonexistent.txt").exists());
9694
9695 let _ = std::fs::remove_dir_all(&tmp);
9696 }
9697
9698 #[tokio::test]
9701 async fn force_recheck_transitions_to_checking() {
9702 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
9704 let storage = make_seeded_storage(&data, 16384);
9705 let config = test_config();
9706
9707 let (atx, amask) = test_alert_channel();
9708 let mut arx = atx.subscribe();
9709 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9710 let handle = TorrentHandle::from_torrent(
9711 meta,
9712 irontide_core::TorrentVersion::V1Only,
9713 None,
9714 dh,
9715 dm,
9716 config,
9717 test_dht_rx(),
9718 test_dht_rx(),
9719 None,
9720 None,
9721 crate::slot_tuner::SlotTuner::disabled(4),
9722 atx,
9723 amask,
9724 None,
9725 None,
9726 test_ban_manager(),
9727 test_ip_filter(),
9728 Arc::new(Vec::new()),
9729 None,
9730 None,
9731 Arc::new(crate::transport::NetworkFactory::tokio()),
9732 None, Arc::new(crate::stats::SessionCounters::new()),
9734 )
9735 .await
9736 .unwrap();
9737
9738 tokio::time::sleep(Duration::from_millis(100)).await;
9740 let stats = handle.stats().await.unwrap();
9741 assert_eq!(stats.state, TorrentState::Seeding, "should start as seeder");
9742
9743 while arx.try_recv().is_ok() {}
9745
9746 handle.force_recheck().await.unwrap();
9748
9749 let mut saw_checking = false;
9752 while let Ok(alert) = arx.try_recv() {
9753 if let crate::alert::AlertKind::StateChanged { new_state, .. } = alert.kind
9754 && new_state == TorrentState::Checking
9755 {
9756 saw_checking = true;
9757 }
9758 }
9759 assert!(
9760 saw_checking,
9761 "should have transitioned through Checking state"
9762 );
9763
9764 handle.shutdown().await.unwrap();
9765 }
9766
9767 #[tokio::test]
9770 async fn force_recheck_completes() {
9771 let data = vec![0xEEu8; 32768]; let meta = make_test_torrent(&data, 16384);
9773 let storage = make_seeded_storage(&data, 16384);
9774 let config = test_config();
9775
9776 let (atx, amask) = test_alert_channel();
9777 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9778 let handle = TorrentHandle::from_torrent(
9779 meta,
9780 irontide_core::TorrentVersion::V1Only,
9781 None,
9782 dh,
9783 dm,
9784 config,
9785 test_dht_rx(),
9786 test_dht_rx(),
9787 None,
9788 None,
9789 crate::slot_tuner::SlotTuner::disabled(4),
9790 atx,
9791 amask,
9792 None,
9793 None,
9794 test_ban_manager(),
9795 test_ip_filter(),
9796 Arc::new(Vec::new()),
9797 None,
9798 None,
9799 Arc::new(crate::transport::NetworkFactory::tokio()),
9800 None, Arc::new(crate::stats::SessionCounters::new()),
9802 )
9803 .await
9804 .unwrap();
9805
9806 tokio::time::sleep(Duration::from_millis(100)).await;
9808 let stats = handle.stats().await.unwrap();
9809 assert_eq!(stats.state, TorrentState::Seeding);
9810 assert_eq!(stats.pieces_have, 2);
9811
9812 handle.force_recheck().await.unwrap();
9814
9815 let stats = handle.stats().await.unwrap();
9816 assert_eq!(
9817 stats.state,
9818 TorrentState::Seeding,
9819 "should return to Seeding after recheck"
9820 );
9821 assert_eq!(stats.pieces_have, 2, "all pieces should still be verified");
9822
9823 handle.shutdown().await.unwrap();
9824 }
9825
9826 #[tokio::test]
9829 async fn rename_file_succeeds() {
9830 let tmp = std::env::temp_dir().join(format!("torrent_rename_{}", std::process::id()));
9832 std::fs::create_dir_all(&tmp).unwrap();
9833
9834 let data = vec![0xFFu8; 16384]; let meta = make_test_torrent(&data, 16384);
9836 let storage = make_seeded_storage(&data, 16384);
9837
9838 std::fs::write(tmp.join("test"), &data).unwrap();
9841
9842 let mut config = test_config();
9843 config.download_dir = tmp.clone();
9844
9845 let (atx, amask) = test_alert_channel();
9846 let mut arx = atx.subscribe();
9847 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9848 let handle = TorrentHandle::from_torrent(
9849 meta,
9850 irontide_core::TorrentVersion::V1Only,
9851 None,
9852 dh,
9853 dm,
9854 config,
9855 test_dht_rx(),
9856 test_dht_rx(),
9857 None,
9858 None,
9859 crate::slot_tuner::SlotTuner::disabled(4),
9860 atx,
9861 amask,
9862 None,
9863 None,
9864 test_ban_manager(),
9865 test_ip_filter(),
9866 Arc::new(Vec::new()),
9867 None,
9868 None,
9869 Arc::new(crate::transport::NetworkFactory::tokio()),
9870 None, Arc::new(crate::stats::SessionCounters::new()),
9872 )
9873 .await
9874 .unwrap();
9875
9876 tokio::time::sleep(Duration::from_millis(100)).await;
9878
9879 while arx.try_recv().is_ok() {}
9881
9882 handle.rename_file(0, "test_renamed".into()).await.unwrap();
9884
9885 assert!(!tmp.join("test").exists(), "old file should be removed");
9887 assert!(tmp.join("test_renamed").exists(), "new file should exist");
9888
9889 let mut saw_renamed = false;
9891 while let Ok(alert) = arx.try_recv() {
9892 if let AlertKind::FileRenamed { index, .. } = alert.kind {
9893 assert_eq!(index, 0);
9894 saw_renamed = true;
9895 }
9896 }
9897 assert!(saw_renamed, "should have received FileRenamed alert");
9898
9899 handle.shutdown().await.unwrap();
9900 let _ = std::fs::remove_dir_all(&tmp);
9901 }
9902
9903 #[tokio::test]
9906 async fn rename_file_invalid_index_errors() {
9907 let data = vec![0xCCu8; 16384]; let meta = make_test_torrent(&data, 16384);
9909 let storage = make_seeded_storage(&data, 16384);
9910 let config = test_config();
9911
9912 let (atx, amask) = test_alert_channel();
9913 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9914 let handle = TorrentHandle::from_torrent(
9915 meta,
9916 irontide_core::TorrentVersion::V1Only,
9917 None,
9918 dh,
9919 dm,
9920 config,
9921 test_dht_rx(),
9922 test_dht_rx(),
9923 None,
9924 None,
9925 crate::slot_tuner::SlotTuner::disabled(4),
9926 atx,
9927 amask,
9928 None,
9929 None,
9930 test_ban_manager(),
9931 test_ip_filter(),
9932 Arc::new(Vec::new()),
9933 None,
9934 None,
9935 Arc::new(crate::transport::NetworkFactory::tokio()),
9936 None, Arc::new(crate::stats::SessionCounters::new()),
9938 )
9939 .await
9940 .unwrap();
9941
9942 tokio::time::sleep(Duration::from_millis(100)).await;
9944
9945 let result = handle.rename_file(99, "bad".into()).await;
9947 assert!(result.is_err(), "should fail for out-of-range file index");
9948
9949 handle.shutdown().await.unwrap();
9950 }
9951
9952 #[tokio::test]
9955 async fn file_completed_alert_fires() {
9956 let data = vec![0xBBu8; 32768]; let meta = make_test_torrent(&data, 16384);
9958 let storage = make_seeded_storage(&data, 16384);
9959 let config = test_config();
9960
9961 let (atx, amask) = test_alert_channel();
9962 let mut arx = atx.subscribe();
9963 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9964 let handle = TorrentHandle::from_torrent(
9965 meta,
9966 irontide_core::TorrentVersion::V1Only,
9967 None,
9968 dh,
9969 dm,
9970 config,
9971 test_dht_rx(),
9972 test_dht_rx(),
9973 None,
9974 None,
9975 crate::slot_tuner::SlotTuner::disabled(4),
9976 atx,
9977 amask,
9978 None,
9979 None,
9980 test_ban_manager(),
9981 test_ip_filter(),
9982 Arc::new(Vec::new()),
9983 None,
9984 None,
9985 Arc::new(crate::transport::NetworkFactory::tokio()),
9986 None, Arc::new(crate::stats::SessionCounters::new()),
9988 )
9989 .await
9990 .unwrap();
9991
9992 tokio::time::sleep(Duration::from_millis(200)).await;
9994
9995 let mut saw_file_completed = false;
9997 while let Ok(alert) = arx.try_recv() {
9998 if let AlertKind::FileCompleted { file_index, .. } = alert.kind {
9999 assert_eq!(file_index, 0, "should be file index 0");
10000 saw_file_completed = true;
10001 }
10002 }
10003 assert!(
10004 saw_file_completed,
10005 "should have received FileCompleted alert"
10006 );
10007
10008 handle.shutdown().await.unwrap();
10009 }
10010
10011 #[test]
10014 fn metadata_failed_alert_fires() {
10015 let info_hash = Id20::from([0u8; 20]);
10017 let alert = crate::alert::Alert::new(AlertKind::MetadataFailed { info_hash });
10018 assert!(
10019 alert
10020 .category()
10021 .contains(crate::alert::AlertCategory::STATUS),
10022 "MetadataFailed should have STATUS category"
10023 );
10024 assert!(
10025 alert
10026 .category()
10027 .contains(crate::alert::AlertCategory::ERROR),
10028 "MetadataFailed should have ERROR category"
10029 );
10030
10031 let (tx, mut rx) = broadcast::channel(16);
10033 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
10034 post_alert(&tx, &mask, AlertKind::MetadataFailed { info_hash });
10035 let received = rx.try_recv().expect("should receive MetadataFailed alert");
10036 assert!(matches!(received.kind, AlertKind::MetadataFailed { .. }));
10037 }
10038
10039 #[tokio::test]
10042 async fn set_max_connections_persists() {
10043 let data = vec![0xAB; 32768];
10044 let meta = make_test_torrent(&data, 16384);
10045 let storage = make_storage(&data, 16384);
10046 let config = test_config();
10047
10048 let (atx, amask) = test_alert_channel();
10049 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10050 let handle = TorrentHandle::from_torrent(
10051 meta,
10052 irontide_core::TorrentVersion::V1Only,
10053 None,
10054 dh,
10055 dm,
10056 config,
10057 test_dht_rx(),
10058 test_dht_rx(),
10059 None,
10060 None,
10061 crate::slot_tuner::SlotTuner::disabled(4),
10062 atx,
10063 amask,
10064 None,
10065 None,
10066 test_ban_manager(),
10067 test_ip_filter(),
10068 Arc::new(Vec::new()),
10069 None,
10070 None,
10071 Arc::new(crate::transport::NetworkFactory::tokio()),
10072 None, Arc::new(crate::stats::SessionCounters::new()),
10074 )
10075 .await
10076 .unwrap();
10077
10078 handle.set_max_connections(10).await.unwrap();
10080 let val = handle.max_connections().await.unwrap();
10081 assert_eq!(val, 10);
10082
10083 handle.set_max_connections(25).await.unwrap();
10085 let val = handle.max_connections().await.unwrap();
10086 assert_eq!(val, 25);
10087
10088 let stats = handle.stats().await.unwrap();
10090 assert_eq!(stats.connections_limit, 25);
10091
10092 handle.shutdown().await.unwrap();
10093 }
10094
10095 #[tokio::test]
10098 async fn max_connections_default() {
10099 let data = vec![0xAB; 32768];
10100 let meta = make_test_torrent(&data, 16384);
10101 let storage = make_storage(&data, 16384);
10102 let config = test_config();
10103 let expected_default = config.max_peers;
10104
10105 let (atx, amask) = test_alert_channel();
10106 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10107 let handle = TorrentHandle::from_torrent(
10108 meta,
10109 irontide_core::TorrentVersion::V1Only,
10110 None,
10111 dh,
10112 dm,
10113 config,
10114 test_dht_rx(),
10115 test_dht_rx(),
10116 None,
10117 None,
10118 crate::slot_tuner::SlotTuner::disabled(4),
10119 atx,
10120 amask,
10121 None,
10122 None,
10123 test_ban_manager(),
10124 test_ip_filter(),
10125 Arc::new(Vec::new()),
10126 None,
10127 None,
10128 Arc::new(crate::transport::NetworkFactory::tokio()),
10129 None, Arc::new(crate::stats::SessionCounters::new()),
10131 )
10132 .await
10133 .unwrap();
10134
10135 let val = handle.max_connections().await.unwrap();
10137 assert_eq!(val, 0);
10138
10139 let stats = handle.stats().await.unwrap();
10141 assert_eq!(stats.connections_limit, expected_default);
10142
10143 handle.shutdown().await.unwrap();
10144 }
10145
10146 #[tokio::test]
10149 async fn set_max_uploads_round_trip() {
10150 let data = vec![0xAB; 32768];
10151 let meta = make_test_torrent(&data, 16384);
10152 let storage = make_storage(&data, 16384);
10153 let config = test_config();
10154
10155 let (atx, amask) = test_alert_channel();
10156 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10157 let handle = TorrentHandle::from_torrent(
10158 meta,
10159 irontide_core::TorrentVersion::V1Only,
10160 None,
10161 dh,
10162 dm,
10163 config,
10164 test_dht_rx(),
10165 test_dht_rx(),
10166 None,
10167 None,
10168 crate::slot_tuner::SlotTuner::disabled(4),
10169 atx,
10170 amask,
10171 None,
10172 None,
10173 test_ban_manager(),
10174 test_ip_filter(),
10175 Arc::new(Vec::new()),
10176 None,
10177 None,
10178 Arc::new(crate::transport::NetworkFactory::tokio()),
10179 None, Arc::new(crate::stats::SessionCounters::new()),
10181 )
10182 .await
10183 .unwrap();
10184
10185 handle.set_max_uploads(8).await.unwrap();
10187 let val = handle.max_uploads().await.unwrap();
10188 assert_eq!(val, 8);
10189
10190 let stats = handle.stats().await.unwrap();
10192 assert_eq!(stats.uploads_limit, 8);
10193
10194 handle.shutdown().await.unwrap();
10195 }
10196
10197 #[tokio::test]
10200 async fn external_ip_detected_alert() {
10201 let data = vec![0xAB; 32768];
10202 let meta = make_test_torrent(&data, 16384);
10203 let storage = make_storage(&data, 16384);
10204 let config = test_config();
10205
10206 let (atx, amask) = test_alert_channel();
10207 let mut arx = atx.subscribe();
10208 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10209 let handle = TorrentHandle::from_torrent(
10210 meta,
10211 irontide_core::TorrentVersion::V1Only,
10212 None,
10213 dh,
10214 dm,
10215 config,
10216 test_dht_rx(),
10217 test_dht_rx(),
10218 None,
10219 None,
10220 crate::slot_tuner::SlotTuner::disabled(4),
10221 atx,
10222 amask,
10223 None,
10224 None,
10225 test_ban_manager(),
10226 test_ip_filter(),
10227 Arc::new(Vec::new()),
10228 None,
10229 None,
10230 Arc::new(crate::transport::NetworkFactory::tokio()),
10231 None, Arc::new(crate::stats::SessionCounters::new()),
10233 )
10234 .await
10235 .unwrap();
10236
10237 while arx.try_recv().is_ok() {}
10239
10240 let test_ip: std::net::IpAddr = "203.0.113.42".parse().unwrap();
10242 handle
10243 .cmd_tx
10244 .send(TorrentCommand::UpdateExternalIp { ip: test_ip })
10245 .await
10246 .unwrap();
10247
10248 tokio::time::sleep(Duration::from_millis(50)).await;
10250
10251 let mut saw_alert = false;
10253 while let Ok(alert) = arx.try_recv() {
10254 if let AlertKind::ExternalIpDetected { ip } = alert.kind {
10255 assert_eq!(ip, test_ip);
10256 saw_alert = true;
10257 }
10258 }
10259 assert!(saw_alert, "should have received ExternalIpDetected alert");
10260
10261 handle.shutdown().await.unwrap();
10262 }
10263
10264 #[tokio::test]
10267 async fn get_peer_info_returns_connected_peers() {
10268 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
10271 let config = test_config();
10272
10273 let (atx, amask) = test_alert_channel();
10274 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10275 let handle = TorrentHandle::from_torrent(
10276 meta.clone(),
10277 irontide_core::TorrentVersion::V1Only,
10278 None,
10279 dh,
10280 dm,
10281 config,
10282 test_dht_rx(),
10283 test_dht_rx(),
10284 None,
10285 None,
10286 crate::slot_tuner::SlotTuner::disabled(4),
10287 atx,
10288 amask,
10289 None,
10290 None,
10291 test_ban_manager(),
10292 test_ip_filter(),
10293 Arc::new(Vec::new()),
10294 None,
10295 None,
10296 Arc::new(crate::transport::NetworkFactory::tokio()),
10297 None, Arc::new(crate::stats::SessionCounters::new()),
10299 )
10300 .await
10301 .unwrap();
10302
10303 let stats = handle.stats().await.unwrap();
10305 let listen_port = stats.peers_connected; let peer_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
10309 let peer_addr = peer_listener.local_addr().unwrap();
10310
10311 handle
10312 .add_peers(vec![peer_addr], PeerSource::Tracker)
10313 .await
10314 .unwrap();
10315
10316 let accept_timeout =
10318 tokio::time::timeout(Duration::from_secs(2), peer_listener.accept()).await;
10319 if let Ok(Ok((mut stream, _))) = accept_timeout {
10320 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
10322 if tokio::time::timeout(Duration::from_millis(500), stream.read_exact(&mut hs_buf))
10323 .await
10324 .is_ok()
10325 {
10326 let hs = Handshake::new(meta.info_hash, Id20::from([0xBB; 20]));
10328 let hs_bytes = hs.to_bytes();
10329 let _ = stream.write_all(&hs_bytes).await;
10330
10331 tokio::time::sleep(Duration::from_millis(200)).await;
10333
10334 let peer_info = handle.get_peer_info().await.unwrap();
10336 if !peer_info.is_empty() {
10338 let p = &peer_info[0];
10339 assert!(p.peer_choking, "peer should be choking us initially");
10341 assert!(
10343 !p.am_choking,
10344 "we should not be choking peer after connect (M107 unconditional unchoke)"
10345 );
10346 assert!(
10347 !p.peer_interested,
10348 "peer should not be interested initially"
10349 );
10350 assert_eq!(p.num_pieces, 0);
10351 assert_eq!(p.source, PeerSource::Tracker);
10352 }
10353 }
10354 }
10355 let _ = handle.get_peer_info().await.unwrap();
10357 assert_eq!(listen_port, 0); handle.shutdown().await.unwrap();
10360 }
10361
10362 #[tokio::test]
10365 async fn get_peer_info_empty_when_no_peers() {
10366 let data = vec![0xAB; 32768];
10367 let meta = make_test_torrent(&data, 16384);
10368 let storage = make_storage(&data, 16384);
10369 let config = test_config();
10370
10371 let (atx, amask) = test_alert_channel();
10372 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10373 let handle = TorrentHandle::from_torrent(
10374 meta,
10375 irontide_core::TorrentVersion::V1Only,
10376 None,
10377 dh,
10378 dm,
10379 config,
10380 test_dht_rx(),
10381 test_dht_rx(),
10382 None,
10383 None,
10384 crate::slot_tuner::SlotTuner::disabled(4),
10385 atx,
10386 amask,
10387 None,
10388 None,
10389 test_ban_manager(),
10390 test_ip_filter(),
10391 Arc::new(Vec::new()),
10392 None,
10393 None,
10394 Arc::new(crate::transport::NetworkFactory::tokio()),
10395 None, Arc::new(crate::stats::SessionCounters::new()),
10397 )
10398 .await
10399 .unwrap();
10400
10401 let peer_info = handle.get_peer_info().await.unwrap();
10402 assert!(peer_info.is_empty(), "should have no peers initially");
10403
10404 handle.shutdown().await.unwrap();
10405 }
10406
10407 #[tokio::test]
10410 async fn get_download_queue_empty_initially() {
10411 let data = vec![0xAB; 32768];
10412 let meta = make_test_torrent(&data, 16384);
10413 let storage = make_storage(&data, 16384);
10414 let config = test_config();
10415
10416 let (atx, amask) = test_alert_channel();
10417 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10418 let handle = TorrentHandle::from_torrent(
10419 meta,
10420 irontide_core::TorrentVersion::V1Only,
10421 None,
10422 dh,
10423 dm,
10424 config,
10425 test_dht_rx(),
10426 test_dht_rx(),
10427 None,
10428 None,
10429 crate::slot_tuner::SlotTuner::disabled(4),
10430 atx,
10431 amask,
10432 None,
10433 None,
10434 test_ban_manager(),
10435 test_ip_filter(),
10436 Arc::new(Vec::new()),
10437 None,
10438 None,
10439 Arc::new(crate::transport::NetworkFactory::tokio()),
10440 None, Arc::new(crate::stats::SessionCounters::new()),
10442 )
10443 .await
10444 .unwrap();
10445
10446 let queue = handle.get_download_queue().await.unwrap();
10447 assert!(
10448 queue.is_empty(),
10449 "download queue should be empty with no active downloads"
10450 );
10451
10452 handle.shutdown().await.unwrap();
10453 }
10454
10455 #[tokio::test]
10458 async fn have_piece_false_initially() {
10459 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10461 let storage = make_storage(&data, 16384);
10462 let config = test_config();
10463
10464 let (atx, amask) = test_alert_channel();
10465 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10466 let handle = TorrentHandle::from_torrent(
10467 meta,
10468 irontide_core::TorrentVersion::V1Only,
10469 None,
10470 dh,
10471 dm,
10472 config,
10473 test_dht_rx(),
10474 test_dht_rx(),
10475 None,
10476 None,
10477 crate::slot_tuner::SlotTuner::disabled(4),
10478 atx,
10479 amask,
10480 None,
10481 None,
10482 test_ban_manager(),
10483 test_ip_filter(),
10484 Arc::new(Vec::new()),
10485 None,
10486 None,
10487 Arc::new(crate::transport::NetworkFactory::tokio()),
10488 None, Arc::new(crate::stats::SessionCounters::new()),
10490 )
10491 .await
10492 .unwrap();
10493
10494 assert!(
10495 !handle.have_piece(0).await.unwrap(),
10496 "piece 0 should not be downloaded initially"
10497 );
10498 assert!(
10499 !handle.have_piece(1).await.unwrap(),
10500 "piece 1 should not be downloaded initially"
10501 );
10502
10503 handle.shutdown().await.unwrap();
10504 }
10505
10506 #[tokio::test]
10509 async fn piece_availability_empty_no_peers() {
10510 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10512 let storage = make_storage(&data, 16384);
10513 let config = test_config();
10514
10515 let (atx, amask) = test_alert_channel();
10516 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10517 let handle = TorrentHandle::from_torrent(
10518 meta,
10519 irontide_core::TorrentVersion::V1Only,
10520 None,
10521 dh,
10522 dm,
10523 config,
10524 test_dht_rx(),
10525 test_dht_rx(),
10526 None,
10527 None,
10528 crate::slot_tuner::SlotTuner::disabled(4),
10529 atx,
10530 amask,
10531 None,
10532 None,
10533 test_ban_manager(),
10534 test_ip_filter(),
10535 Arc::new(Vec::new()),
10536 None,
10537 None,
10538 Arc::new(crate::transport::NetworkFactory::tokio()),
10539 None, Arc::new(crate::stats::SessionCounters::new()),
10541 )
10542 .await
10543 .unwrap();
10544
10545 let avail = handle.piece_availability().await.unwrap();
10546 assert_eq!(avail.len(), 2, "should have availability for 2 pieces");
10547 assert!(
10548 avail.iter().all(|&c| c == 0),
10549 "all availability counts should be 0 with no peers"
10550 );
10551
10552 handle.shutdown().await.unwrap();
10553 }
10554
10555 #[tokio::test]
10558 async fn file_progress_zeros_initially() {
10559 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10561 let storage = make_storage(&data, 16384);
10562 let config = test_config();
10563
10564 let (atx, amask) = test_alert_channel();
10565 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10566 let handle = TorrentHandle::from_torrent(
10567 meta,
10568 irontide_core::TorrentVersion::V1Only,
10569 None,
10570 dh,
10571 dm,
10572 config,
10573 test_dht_rx(),
10574 test_dht_rx(),
10575 None,
10576 None,
10577 crate::slot_tuner::SlotTuner::disabled(4),
10578 atx,
10579 amask,
10580 None,
10581 None,
10582 test_ban_manager(),
10583 test_ip_filter(),
10584 Arc::new(Vec::new()),
10585 None,
10586 None,
10587 Arc::new(crate::transport::NetworkFactory::tokio()),
10588 None, Arc::new(crate::stats::SessionCounters::new()),
10590 )
10591 .await
10592 .unwrap();
10593
10594 let progress = handle.file_progress().await.unwrap();
10595 assert_eq!(progress.len(), 1, "single-file torrent should have 1 entry");
10596 assert_eq!(progress[0], 0, "no bytes should be downloaded initially");
10597
10598 handle.shutdown().await.unwrap();
10599 }
10600
10601 fn make_test_torrent_multi(
10605 data: &[u8],
10606 piece_length: u64,
10607 file_lengths: &[u64],
10608 ) -> TorrentMetaV1 {
10609 use serde::Serialize;
10610
10611 #[derive(Serialize)]
10612 struct FileE {
10613 length: u64,
10614 path: Vec<String>,
10615 }
10616
10617 #[derive(Serialize)]
10618 struct Info<'a> {
10619 name: &'a str,
10620 #[serde(rename = "piece length")]
10621 piece_length: u64,
10622 #[serde(with = "serde_bytes")]
10623 pieces: &'a [u8],
10624 files: Vec<FileE>,
10625 }
10626
10627 #[derive(Serialize)]
10628 struct Torrent<'a> {
10629 info: Info<'a>,
10630 }
10631
10632 let mut pieces = Vec::new();
10633 let mut offset = 0;
10634 while offset < data.len() {
10635 let end = (offset + piece_length as usize).min(data.len());
10636 let hash = irontide_core::sha1(&data[offset..end]);
10637 pieces.extend_from_slice(hash.as_bytes());
10638 offset = end;
10639 }
10640
10641 let files: Vec<FileE> = file_lengths
10642 .iter()
10643 .enumerate()
10644 .map(|(i, &len)| FileE {
10645 length: len,
10646 path: vec![format!("file{i}.bin")],
10647 })
10648 .collect();
10649
10650 let t = Torrent {
10651 info: Info {
10652 name: "test_multi",
10653 piece_length,
10654 pieces: &pieces,
10655 files,
10656 },
10657 };
10658
10659 let bytes = irontide_bencode::to_bytes(&t).unwrap();
10660 torrent_from_bytes(&bytes).unwrap()
10661 }
10662
10663 #[tokio::test]
10664 async fn file_progress_length_matches_file_count() {
10665 let data = vec![0xCD; 32768];
10667 let file_lengths = [10000u64, 20000, 2768];
10668 let meta = make_test_torrent_multi(&data, 16384, &file_lengths);
10669 let storage = make_storage(&data, 16384);
10670 let config = test_config();
10671
10672 let (atx, amask) = test_alert_channel();
10673 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10674 let handle = TorrentHandle::from_torrent(
10675 meta,
10676 irontide_core::TorrentVersion::V1Only,
10677 None,
10678 dh,
10679 dm,
10680 config,
10681 test_dht_rx(),
10682 test_dht_rx(),
10683 None,
10684 None,
10685 crate::slot_tuner::SlotTuner::disabled(4),
10686 atx,
10687 amask,
10688 None,
10689 None,
10690 test_ban_manager(),
10691 test_ip_filter(),
10692 Arc::new(Vec::new()),
10693 None,
10694 None,
10695 Arc::new(crate::transport::NetworkFactory::tokio()),
10696 None, Arc::new(crate::stats::SessionCounters::new()),
10698 )
10699 .await
10700 .unwrap();
10701
10702 let progress = handle.file_progress().await.unwrap();
10703 assert_eq!(
10704 progress.len(),
10705 3,
10706 "multi-file torrent should have 3 entries"
10707 );
10708 assert!(
10709 progress.iter().all(|&b| b == 0),
10710 "all progress should be 0 initially"
10711 );
10712
10713 handle.shutdown().await.unwrap();
10714 }
10715
10716 #[tokio::test]
10719 async fn is_valid_true_for_active() {
10720 let data = vec![0xAB; 32768];
10721 let meta = make_test_torrent(&data, 16384);
10722 let storage = make_storage(&data, 16384);
10723 let config = test_config();
10724
10725 let (atx, amask) = test_alert_channel();
10726 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10727 let handle = TorrentHandle::from_torrent(
10728 meta,
10729 irontide_core::TorrentVersion::V1Only,
10730 None,
10731 dh,
10732 dm,
10733 config,
10734 test_dht_rx(),
10735 test_dht_rx(),
10736 None,
10737 None,
10738 crate::slot_tuner::SlotTuner::disabled(4),
10739 atx,
10740 amask,
10741 None,
10742 None,
10743 test_ban_manager(),
10744 test_ip_filter(),
10745 Arc::new(Vec::new()),
10746 None,
10747 None,
10748 Arc::new(crate::transport::NetworkFactory::tokio()),
10749 None, Arc::new(crate::stats::SessionCounters::new()),
10751 )
10752 .await
10753 .unwrap();
10754
10755 assert!(
10756 handle.is_valid(),
10757 "handle should be valid while torrent actor is alive"
10758 );
10759
10760 handle.shutdown().await.unwrap();
10761 }
10762
10763 #[tokio::test]
10766 async fn is_valid_false_after_remove() {
10767 let data = vec![0xAB; 32768];
10768 let meta = make_test_torrent(&data, 16384);
10769 let storage = make_storage(&data, 16384);
10770 let config = test_config();
10771
10772 let (atx, amask) = test_alert_channel();
10773 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10774 let handle = TorrentHandle::from_torrent(
10775 meta,
10776 irontide_core::TorrentVersion::V1Only,
10777 None,
10778 dh,
10779 dm,
10780 config,
10781 test_dht_rx(),
10782 test_dht_rx(),
10783 None,
10784 None,
10785 crate::slot_tuner::SlotTuner::disabled(4),
10786 atx,
10787 amask,
10788 None,
10789 None,
10790 test_ban_manager(),
10791 test_ip_filter(),
10792 Arc::new(Vec::new()),
10793 None,
10794 None,
10795 Arc::new(crate::transport::NetworkFactory::tokio()),
10796 None, Arc::new(crate::stats::SessionCounters::new()),
10798 )
10799 .await
10800 .unwrap();
10801
10802 assert!(handle.is_valid());
10803
10804 handle.shutdown().await.unwrap();
10806
10807 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
10809
10810 assert!(
10811 !handle.is_valid(),
10812 "handle should be invalid after shutdown"
10813 );
10814 }
10815
10816 #[tokio::test]
10819 async fn clear_error_resets() {
10820 let data = vec![0xAB; 32768];
10821 let meta = make_test_torrent(&data, 16384);
10822 let storage = make_storage(&data, 16384);
10823 let config = test_config();
10824
10825 let (atx, amask) = test_alert_channel();
10826 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10827 let handle = TorrentHandle::from_torrent(
10828 meta,
10829 irontide_core::TorrentVersion::V1Only,
10830 None,
10831 dh,
10832 dm,
10833 config,
10834 test_dht_rx(),
10835 test_dht_rx(),
10836 None,
10837 None,
10838 crate::slot_tuner::SlotTuner::disabled(4),
10839 atx,
10840 amask,
10841 None,
10842 None,
10843 test_ban_manager(),
10844 test_ip_filter(),
10845 Arc::new(Vec::new()),
10846 None,
10847 None,
10848 Arc::new(crate::transport::NetworkFactory::tokio()),
10849 None, Arc::new(crate::stats::SessionCounters::new()),
10851 )
10852 .await
10853 .unwrap();
10854
10855 let stats = handle.stats().await.unwrap();
10857 assert!(stats.error.is_empty());
10858 assert_eq!(stats.error_file, -1);
10859
10860 handle.clear_error().await.unwrap();
10862
10863 let stats = handle.stats().await.unwrap();
10864 assert!(stats.error.is_empty());
10865 assert_eq!(stats.error_file, -1);
10866
10867 handle.shutdown().await.unwrap();
10868 }
10869
10870 #[tokio::test]
10873 async fn flags_round_trip() {
10874 let data = vec![0xAB; 32768];
10875 let meta = make_test_torrent(&data, 16384);
10876 let storage = make_storage(&data, 16384);
10877 let config = test_config();
10878
10879 let (atx, amask) = test_alert_channel();
10880 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10881 let handle = TorrentHandle::from_torrent(
10882 meta,
10883 irontide_core::TorrentVersion::V1Only,
10884 None,
10885 dh,
10886 dm,
10887 config,
10888 test_dht_rx(),
10889 test_dht_rx(),
10890 None,
10891 None,
10892 crate::slot_tuner::SlotTuner::disabled(4),
10893 atx,
10894 amask,
10895 None,
10896 None,
10897 test_ban_manager(),
10898 test_ip_filter(),
10899 Arc::new(Vec::new()),
10900 None,
10901 None,
10902 Arc::new(crate::transport::NetworkFactory::tokio()),
10903 None, Arc::new(crate::stats::SessionCounters::new()),
10905 )
10906 .await
10907 .unwrap();
10908
10909 let initial = handle.flags().await.unwrap();
10911 assert!(!initial.contains(crate::types::TorrentFlags::PAUSED));
10912 assert!(!initial.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10913 assert!(!initial.contains(crate::types::TorrentFlags::SUPER_SEEDING));
10914
10915 handle
10917 .set_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
10918 .await
10919 .unwrap();
10920 let after_set = handle.flags().await.unwrap();
10921 assert!(after_set.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10922
10923 handle
10925 .unset_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
10926 .await
10927 .unwrap();
10928 let after_unset = handle.flags().await.unwrap();
10929 assert!(!after_unset.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10930
10931 assert!(!handle.is_sequential_download().await.unwrap());
10933
10934 handle.shutdown().await.unwrap();
10935 }
10936
10937 #[tokio::test]
10940 async fn connect_peer_no_error() {
10941 let data = vec![0xAB; 32768];
10942 let meta = make_test_torrent(&data, 16384);
10943 let storage = make_storage(&data, 16384);
10944 let config = test_config();
10945
10946 let (atx, amask) = test_alert_channel();
10947 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10948 let handle = TorrentHandle::from_torrent(
10949 meta,
10950 irontide_core::TorrentVersion::V1Only,
10951 None,
10952 dh,
10953 dm,
10954 config,
10955 test_dht_rx(),
10956 test_dht_rx(),
10957 None,
10958 None,
10959 crate::slot_tuner::SlotTuner::disabled(4),
10960 atx,
10961 amask,
10962 None,
10963 None,
10964 test_ban_manager(),
10965 test_ip_filter(),
10966 Arc::new(Vec::new()),
10967 None,
10968 None,
10969 Arc::new(crate::transport::NetworkFactory::tokio()),
10970 None, Arc::new(crate::stats::SessionCounters::new()),
10972 )
10973 .await
10974 .unwrap();
10975
10976 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
10979 handle.connect_peer(addr).await.unwrap();
10980
10981 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
10983
10984 handle.shutdown().await.unwrap();
10985 }
10986
10987 fn make_test_meta_v2(
10991 piece_hashes: &[irontide_core::Id32],
10992 file_root: irontide_core::Id32,
10993 piece_length: u64,
10994 file_length: u64,
10995 ) -> irontide_core::TorrentMetaV2 {
10996 use std::collections::BTreeMap;
10997
10998 let mut layer_bytes = Vec::with_capacity(piece_hashes.len() * 32);
11000 for h in piece_hashes {
11001 layer_bytes.extend_from_slice(&h.0);
11002 }
11003
11004 let mut piece_layers = BTreeMap::new();
11005 piece_layers.insert(file_root, layer_bytes);
11006
11007 let file_tree = irontide_core::FileTreeNode::Directory({
11008 let mut children = BTreeMap::new();
11009 children.insert(
11010 "test.dat".to_string(),
11011 irontide_core::FileTreeNode::File(irontide_core::V2FileAttr {
11012 length: file_length,
11013 pieces_root: Some(file_root),
11014 }),
11015 );
11016 children
11017 });
11018
11019 irontide_core::TorrentMetaV2 {
11020 info_hashes: irontide_core::InfoHashes::v2_only(irontide_core::Id32::ZERO),
11021 info_bytes: None,
11022 announce: None,
11023 announce_list: None,
11024 comment: None,
11025 created_by: None,
11026 creation_date: None,
11027 info: irontide_core::InfoDictV2 {
11028 name: "test".to_string(),
11029 piece_length,
11030 meta_version: 2,
11031 file_tree,
11032 ssl_cert: None,
11033 },
11034 piece_layers,
11035 ssl_cert: None,
11036 }
11037 }
11038
11039 #[test]
11040 fn test_serve_hashes_v2_piece_layer() {
11041 let hashes: Vec<irontide_core::Id32> = (0..4u8)
11044 .map(|i| {
11045 let mut h = [0u8; 32];
11046 h[0] = i;
11047 irontide_core::Id32(h)
11048 })
11049 .collect();
11050 let file_root = irontide_core::Id32([0xAA; 32]);
11051 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11052 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11053
11054 let request = irontide_core::HashRequest {
11055 file_root,
11056 base: 0, index: 0,
11058 count: 4,
11059 proof_layers: 0,
11060 };
11061
11062 let result = serve_hashes(
11063 Some(&meta),
11064 irontide_core::TorrentVersion::V2Only,
11065 Some(&lengths),
11066 &request,
11067 );
11068 let served = result.expect("should serve hashes");
11069 assert_eq!(served.len(), 4);
11070 for (i, h) in served.iter().enumerate() {
11071 assert_eq!(h.0[0], i as u8);
11072 }
11073 }
11074
11075 #[test]
11076 fn test_serve_hashes_rejects_v1_only() {
11077 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
11078 let file_root = irontide_core::Id32([0xAA; 32]);
11079 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
11080 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
11081
11082 let request = irontide_core::HashRequest {
11083 file_root,
11084 base: 0,
11085 index: 0,
11086 count: 1,
11087 proof_layers: 0,
11088 };
11089
11090 let result = serve_hashes(
11091 Some(&meta),
11092 irontide_core::TorrentVersion::V1Only,
11093 Some(&lengths),
11094 &request,
11095 );
11096 assert!(result.is_none(), "V1Only should reject hash requests");
11097 }
11098
11099 #[test]
11100 fn test_serve_hashes_rejects_unknown_root() {
11101 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
11102 let file_root = irontide_core::Id32([0xAA; 32]);
11103 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
11104 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
11105
11106 let unknown_root = irontide_core::Id32([0xFF; 32]);
11108 let request = irontide_core::HashRequest {
11109 file_root: unknown_root,
11110 base: 0,
11111 index: 0,
11112 count: 1,
11113 proof_layers: 0,
11114 };
11115
11116 let result = serve_hashes(
11117 Some(&meta),
11118 irontide_core::TorrentVersion::V2Only,
11119 Some(&lengths),
11120 &request,
11121 );
11122 assert!(result.is_none(), "unknown file_root should reject");
11123 }
11124
11125 #[test]
11126 fn test_serve_hashes_rejects_out_of_bounds() {
11127 let hashes: Vec<irontide_core::Id32> =
11129 (0..2u8).map(|i| irontide_core::Id32([i; 32])).collect();
11130 let file_root = irontide_core::Id32([0xAA; 32]);
11131 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 2);
11132 let lengths = Lengths::new(16384 * 2, 16384, DEFAULT_CHUNK_SIZE);
11133
11134 let request = irontide_core::HashRequest {
11136 file_root,
11137 base: 0,
11138 index: 5,
11139 count: 1,
11140 proof_layers: 0,
11141 };
11142
11143 let result = serve_hashes(
11144 Some(&meta),
11145 irontide_core::TorrentVersion::V2Only,
11146 Some(&lengths),
11147 &request,
11148 );
11149 assert!(result.is_none(), "out-of-bounds index should reject");
11150 }
11151
11152 #[test]
11153 fn test_serve_hashes_includes_proofs() {
11154 let hashes: Vec<irontide_core::Id32> =
11157 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
11158 let file_root = irontide_core::Id32([0xAA; 32]);
11159 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11160 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11161
11162 let request = irontide_core::HashRequest {
11164 file_root,
11165 base: 0,
11166 index: 0,
11167 count: 1,
11168 proof_layers: 1,
11169 };
11170
11171 let result = serve_hashes(
11172 Some(&meta),
11173 irontide_core::TorrentVersion::V2Only,
11174 Some(&lengths),
11175 &request,
11176 );
11177 let served = result.expect("should serve hashes with proofs");
11178 assert_eq!(served.len(), 2, "should have 1 data hash + 1 proof hash");
11180 assert_eq!(served[0], hashes[0]);
11182 assert_eq!(served[1], hashes[1]);
11184 }
11185
11186 #[test]
11187 fn test_serve_hashes_proof_with_batch() {
11188 let hashes: Vec<irontide_core::Id32> =
11203 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
11204 let file_root = irontide_core::Id32([0xAA; 32]);
11205 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11206 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11207
11208 let request = irontide_core::HashRequest {
11209 file_root,
11210 base: 0,
11211 index: 0,
11212 count: 2,
11213 proof_layers: 1,
11214 };
11215
11216 let result = serve_hashes(
11217 Some(&meta),
11218 irontide_core::TorrentVersion::V2Only,
11219 Some(&lengths),
11220 &request,
11221 );
11222 let served = result.expect("should serve hashes with batch proof");
11223 assert_eq!(served.len(), 3, "should have 2 data hashes + 1 uncle hash");
11225 assert_eq!(served[0], hashes[0]);
11227 assert_eq!(served[1], hashes[1]);
11228 let tree = irontide_core::MerkleTree::from_leaves(&hashes);
11231 let expected_uncle = tree.layer(1)[1]; assert_eq!(served[2], expected_uncle);
11233
11234 let sub_root = irontide_core::MerkleTree::root_from_hashes(&served[..2]);
11237 let uncle_hashes = &served[2..];
11238 let leaf_index = request.index as usize / 2; assert!(
11240 irontide_core::MerkleTree::verify_proof(
11241 tree.root(),
11242 sub_root,
11243 leaf_index,
11244 uncle_hashes
11245 ),
11246 "subtree proof should verify against tree root"
11247 );
11248 }
11249
11250 #[test]
11251 fn is_i2p_synthetic_addr_detects_240_range() {
11252 assert!(is_i2p_synthetic_addr(&"240.0.0.1:1".parse().unwrap()));
11253 assert!(is_i2p_synthetic_addr(
11254 &"255.255.255.255:65535".parse().unwrap()
11255 ));
11256 assert!(!is_i2p_synthetic_addr(&"192.168.1.1:6881".parse().unwrap()));
11257 assert!(!is_i2p_synthetic_addr(&"[::1]:6881".parse().unwrap()));
11258 }
11259
11260 #[test]
11261 fn v6_retry_delay_progression() {
11262 let expected_ms = [100, 200, 400, 800, 1600, 3200, 5000, 5000, 5000, 5000, 5000];
11264 for (count, &expected) in expected_ms.iter().enumerate() {
11265 let delay_ms = {
11266 let base_ms: u64 = 100;
11267 let max_ms: u64 = 5000;
11268 base_ms
11269 .saturating_mul(1u64.checked_shl(count as u32).unwrap_or(u64::MAX))
11270 .min(max_ms)
11271 };
11272 assert_eq!(
11273 delay_ms, expected,
11274 "count={count}: expected {expected}ms, got {delay_ms}ms"
11275 );
11276 }
11277 }
11278
11279 #[test]
11282 fn peer_backoff_exponential() {
11283 let expected_ms: Vec<u64> = vec![400, 800, 1600, 3200, 6400, 12800, 25600, 30000, 30000];
11286 for (i, &expected) in expected_ms.iter().enumerate() {
11287 let attempt = (i as u32) + 1; let delay_ms = 200u64.saturating_mul(1u64 << attempt.min(10)).min(30_000);
11289 assert_eq!(
11290 delay_ms, expected,
11291 "attempt={attempt}: expected {expected}ms, got {delay_ms}ms"
11292 );
11293 }
11294 }
11295
11296 #[test]
11297 fn peer_backoff_clears_on_data() {
11298 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
11301 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
11302
11303 assert!(!backoff.contains_key(&addr));
11305
11306 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
11308 let next = attempt.saturating_add(1);
11309 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
11310 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
11311 backoff.insert(addr, (earliest, next));
11312 assert_eq!(backoff.get(&addr).unwrap().1, 1);
11313
11314 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
11316 let next = attempt.saturating_add(1);
11317 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
11318 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
11319 backoff.insert(addr, (earliest, next));
11320 assert_eq!(backoff.get(&addr).unwrap().1, 2);
11321
11322 backoff.remove(&addr);
11324 assert!(!backoff.contains_key(&addr));
11325 }
11326
11327 #[test]
11328 fn backoff_prevents_hammering() {
11329 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
11331 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
11332
11333 let future = std::time::Instant::now() + Duration::from_secs(10);
11335 backoff.insert(addr, (future, 3));
11336
11337 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
11339 assert!(std::time::Instant::now() < next_attempt);
11340 }
11341
11342 let past = std::time::Instant::now() - Duration::from_secs(1);
11344 backoff.insert(addr, (past, 3));
11345 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
11346 assert!(std::time::Instant::now() >= next_attempt);
11347 }
11348 }
11349
11350 #[test]
11351 fn max_in_flight_formula_updated() {
11352 let formula = |connected: usize, num_pieces: u32| -> usize {
11354 let calculated = 512usize.max(connected.saturating_mul(4));
11355 calculated.min(num_pieces as usize / 2).max(512)
11356 };
11357
11358 assert_eq!(formula(10, 2000), 512);
11360
11361 assert_eq!(formula(200, 2000), 800);
11363
11364 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);
11376
11377 assert_eq!(formula(100, 0), 512);
11379 }
11380
11381 #[test]
11384 fn should_attempt_holepunch_reason_classification() {
11385 assert!(should_attempt_holepunch("connection refused"));
11387 assert!(should_attempt_holepunch("Connection refused"));
11388 assert!(should_attempt_holepunch("timed out"));
11389 assert!(should_attempt_holepunch("Connection reset by peer"));
11390 assert!(should_attempt_holepunch("connection reset by peer"));
11391 assert!(!should_attempt_holepunch(
11393 "holepunch TCP connect failed: Connection refused"
11394 ));
11395 assert!(!should_attempt_holepunch("peer banned"));
11397 assert!(!should_attempt_holepunch("protocol error"));
11398 assert!(!should_attempt_holepunch(""));
11399 }
11400
11401 #[test]
11402 fn holepunch_initiation_on_connect_failure() {
11403 assert!(should_attempt_holepunch("connection refused"));
11405 }
11406
11407 #[test]
11408 fn holepunch_cooldown_prevents_retry() {
11409 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
11410 let addr: SocketAddr = "127.0.0.1:6881".parse().expect("valid test addr");
11411 let now = Instant::now();
11412 cooldowns.insert(addr, now);
11413 assert!(cooldowns.contains_key(&addr));
11415 }
11416
11417 #[test]
11418 fn holepunch_cooldown_overflow_skips() {
11419 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
11420 let now = Instant::now();
11421 for i in 0..256u16 {
11422 let addr: SocketAddr = format!("10.0.{}.{}:6881", i / 256, i % 256)
11423 .parse()
11424 .expect("valid test addr");
11425 cooldowns.insert(addr, now);
11426 }
11427 assert_eq!(cooldowns.len(), HOLEPUNCH_MAX_TRACKED);
11428 }
11430
11431 #[test]
11432 fn holepunch_skipped_when_disabled() {
11433 assert!(should_attempt_holepunch("connection refused"));
11436 }
11438
11439 #[test]
11440 fn holepunch_not_triggered_on_ban() {
11441 assert!(!should_attempt_holepunch("peer banned"));
11442 assert!(!should_attempt_holepunch("banned for bad data"));
11443 }
11444
11445 fn make_multi_file_meta(files: &[(u64, &str)], piece_length: u64) -> TorrentMetaV1 {
11449 let total_length: u64 = files.iter().map(|(len, _)| *len).sum();
11450 let num_pieces = total_length.div_ceil(piece_length) as usize;
11451 let file_entries: Vec<irontide_core::FileEntry> = files
11452 .iter()
11453 .map(|(length, name)| irontide_core::FileEntry {
11454 length: *length,
11455 path: vec![name.to_string()],
11456 attr: None,
11457 mtime: None,
11458 symlink_path: None,
11459 })
11460 .collect();
11461 TorrentMetaV1 {
11462 info_hash: Id20([0u8; 20]),
11463 announce: None,
11464 announce_list: None,
11465 comment: None,
11466 created_by: None,
11467 creation_date: None,
11468 info: irontide_core::InfoDict {
11469 name: "test".to_string(),
11470 piece_length,
11471 pieces: vec![0u8; num_pieces * 20],
11472 length: None,
11473 files: Some(file_entries),
11474 private: None,
11475 source: None,
11476 ssl_cert: None,
11477 similar: Vec::new(),
11478 collections: Vec::new(),
11479 },
11480 url_list: Vec::new(),
11481 httpseeds: Vec::new(),
11482 info_bytes: None,
11483 ssl_cert: None,
11484 }
11485 }
11486
11487 #[test]
11488 fn cached_files_populated_on_registration() {
11489 let meta = make_multi_file_meta(&[(100, "a.txt"), (200, "b.txt"), (50, "c.txt")], 100);
11495 let lengths = Lengths::new(350, 100, 16384);
11496 let cached = build_cached_file_info(&meta, &lengths);
11497
11498 assert_eq!(cached.entries.len(), 3);
11499
11500 assert_eq!(cached.entries[0].index, 0);
11501 assert_eq!(cached.entries[0].length, 100);
11502 assert_eq!(cached.entries[0].first_piece, 0);
11503 assert_eq!(cached.entries[0].last_piece, 0);
11504
11505 assert_eq!(cached.entries[1].index, 1);
11506 assert_eq!(cached.entries[1].length, 200);
11507 assert_eq!(cached.entries[1].first_piece, 1);
11508 assert_eq!(cached.entries[1].last_piece, 2);
11509
11510 assert_eq!(cached.entries[2].index, 2);
11511 assert_eq!(cached.entries[2].length, 50);
11512 assert_eq!(cached.entries[2].first_piece, 3);
11513 assert_eq!(cached.entries[2].last_piece, 3);
11514 }
11515
11516 #[test]
11517 fn cached_files_single_file_torrent() {
11518 let meta = TorrentMetaV1 {
11521 info_hash: Id20([0u8; 20]),
11522 announce: None,
11523 announce_list: None,
11524 comment: None,
11525 created_by: None,
11526 creation_date: None,
11527 info: irontide_core::InfoDict {
11528 name: "single.bin".to_string(),
11529 piece_length: 100,
11530 pieces: vec![0u8; 5 * 20],
11531 length: Some(500),
11532 files: None,
11533 private: None,
11534 source: None,
11535 ssl_cert: None,
11536 similar: Vec::new(),
11537 collections: Vec::new(),
11538 },
11539 url_list: Vec::new(),
11540 httpseeds: Vec::new(),
11541 info_bytes: None,
11542 ssl_cert: None,
11543 };
11544 let lengths = Lengths::new(500, 100, 16384);
11545 let cached = build_cached_file_info(&meta, &lengths);
11546
11547 assert_eq!(cached.entries.len(), 1);
11548 assert_eq!(cached.entries[0].index, 0);
11549 assert_eq!(cached.entries[0].length, 500);
11550 assert_eq!(cached.entries[0].first_piece, 0);
11551 assert_eq!(cached.entries[0].last_piece, 4);
11552 }
11553
11554 use crate::piece_reservation::{AtomicPieceStates, PieceState, StealCandidates};
11561 use irontide_storage::Bitfield;
11562
11563 fn steal_populate_scan(states: &AtomicPieceStates, sc: &StealCandidates) -> u32 {
11567 let mut pushed = 0u32;
11568 let num = states.len();
11569 for piece in 0..num {
11570 let state = states.get(piece);
11571 if state == PieceState::Reserved {
11572 sc.push(piece);
11573 pushed = pushed.saturating_add(1);
11574 }
11575 }
11576 pushed
11577 }
11578
11579 fn all_wanted(n: u32) -> Bitfield {
11580 let mut bf = Bitfield::new(n);
11581 for i in 0..n {
11582 bf.set(i);
11583 }
11584 bf
11585 }
11586
11587 #[test]
11588 fn steal_populate_pushes_reserved_pieces() {
11589 let n = 10;
11590 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11591 let sc = StealCandidates::new();
11592
11593 assert!(states.try_reserve(2));
11595 assert!(states.try_reserve(5));
11596 assert!(states.try_reserve(7));
11597
11598 let pushed = steal_populate_scan(&states, &sc);
11599 assert_eq!(pushed, 3, "should push exactly the 3 reserved pieces");
11600
11601 let mut popped = Vec::new();
11603 while let Some(p) = sc.pop() {
11604 popped.push(p);
11605 }
11606 popped.sort_unstable();
11607 assert_eq!(popped, vec![2, 5, 7]);
11608 }
11609
11610 #[test]
11611 fn steal_populate_skips_non_reserved_states() {
11612 let n = 8;
11613 let mut have = Bitfield::new(n);
11614 have.set(0); let mut wanted = all_wanted(n);
11616 wanted.clear(1); let states = AtomicPieceStates::new(n, &have, &wanted);
11619 let sc = StealCandidates::new();
11620
11621 assert!(states.try_reserve(3));
11623
11624 let pushed = steal_populate_scan(&states, &sc);
11625 assert_eq!(pushed, 1, "only piece 3 (Reserved) should be pushed");
11626
11627 assert_eq!(sc.pop(), Some(3));
11628 assert_eq!(sc.pop(), None);
11629 }
11630
11631 #[test]
11632 fn steal_populate_deduplicates() {
11633 let n = 4;
11634 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11635 let sc = StealCandidates::new();
11636
11637 assert!(states.try_reserve(1));
11638 assert!(states.try_reserve(2));
11639
11640 let pushed1 = steal_populate_scan(&states, &sc);
11642 assert_eq!(pushed1, 2);
11643
11644 let pushed2 = steal_populate_scan(&states, &sc);
11647 assert_eq!(pushed2, 2, "scan still reports 2 reserved pieces");
11648
11649 let mut count = 0u32;
11650 while sc.pop().is_some() {
11651 count = count.saturating_add(1);
11652 }
11653 assert_eq!(count, 2, "dedup means only 2 entries despite 2 scans");
11654 }
11655
11656 #[test]
11657 fn steal_populate_skips_completed_pieces() {
11658 let n = 5;
11659 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11660 let sc = StealCandidates::new();
11661
11662 for i in 0..n {
11664 assert!(states.try_reserve(i));
11665 }
11666
11667 states.mark_complete(1);
11669 states.mark_complete(3);
11670
11671 let pushed = steal_populate_scan(&states, &sc);
11672 assert_eq!(pushed, 3, "3 pieces still Reserved (0, 2, 4)");
11673
11674 let mut popped = Vec::new();
11675 while let Some(p) = sc.pop() {
11676 popped.push(p);
11677 }
11678 popped.sort_unstable();
11679 assert_eq!(popped, vec![0, 2, 4]);
11680 }
11681
11682 #[test]
11683 fn steal_populate_empty_when_no_reserved() {
11684 let n = 6;
11685 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11686 let sc = StealCandidates::new();
11687
11688 let pushed = steal_populate_scan(&states, &sc);
11690 assert_eq!(pushed, 0);
11691 assert_eq!(sc.pop(), None);
11692 }
11693
11694 #[test]
11695 fn steal_populate_with_endgame_pieces() {
11696 let n = 4;
11698 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11699 let sc = StealCandidates::new();
11700
11701 assert!(states.try_reserve(0));
11702 assert!(states.try_reserve(1));
11703 states.transition_to_endgame(1);
11704
11705 let pushed = steal_populate_scan(&states, &sc);
11706 assert_eq!(
11707 pushed, 1,
11708 "only piece 0 (Reserved) should be pushed, not piece 1 (Endgame)"
11709 );
11710 assert_eq!(sc.pop(), Some(0));
11711 assert_eq!(sc.pop(), None);
11712 }
11713
11714 #[test]
11719 fn sync_piece_states_marks_unwanted_on_skip() {
11720 let n = 8;
11721 let mut wanted = all_wanted(n);
11722 wanted.clear(2);
11723 wanted.clear(3);
11724 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11725 assert_eq!(states.get(2), PieceState::Available);
11728 assert_eq!(states.get(3), PieceState::Available);
11729
11730 for piece in 0..n {
11732 let w = wanted.get(piece);
11733 let current = states.get(piece);
11734 if !w && current == PieceState::Available {
11735 states.mark_unwanted(piece);
11736 } else if w && current == PieceState::Unwanted {
11737 states.mark_available(piece);
11738 }
11739 }
11740
11741 assert_eq!(states.get(0), PieceState::Available);
11742 assert_eq!(states.get(2), PieceState::Unwanted);
11743 assert_eq!(states.get(3), PieceState::Unwanted);
11744 assert_eq!(states.get(4), PieceState::Available);
11745 }
11746
11747 #[test]
11748 fn sync_piece_states_restores_available_on_unskip() {
11749 let n = 6;
11750 let mut initial_wanted = all_wanted(n);
11751 initial_wanted.clear(1);
11752 initial_wanted.clear(4);
11753 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &initial_wanted);
11754 assert_eq!(states.get(1), PieceState::Unwanted);
11755 assert_eq!(states.get(4), PieceState::Unwanted);
11756
11757 let new_wanted = all_wanted(n);
11759 for piece in 0..n {
11760 let w = new_wanted.get(piece);
11761 let current = states.get(piece);
11762 if !w && current == PieceState::Available {
11763 states.mark_unwanted(piece);
11764 } else if w && current == PieceState::Unwanted {
11765 states.mark_available(piece);
11766 }
11767 }
11768
11769 assert_eq!(states.get(1), PieceState::Available);
11770 assert_eq!(states.get(4), PieceState::Available);
11771 }
11772
11773 #[test]
11774 fn sync_piece_states_shared_piece_stays_available() {
11775 let n = 4;
11779 let mut wanted = all_wanted(n);
11780 wanted.clear(0); let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11783
11784 for piece in 0..n {
11785 let w = wanted.get(piece);
11786 let current = states.get(piece);
11787 if !w && current == PieceState::Available {
11788 states.mark_unwanted(piece);
11789 } else if w && current == PieceState::Unwanted {
11790 states.mark_available(piece);
11791 }
11792 }
11793
11794 assert_eq!(states.get(0), PieceState::Unwanted);
11795 assert_eq!(
11796 states.get(1),
11797 PieceState::Available,
11798 "shared piece stays Available"
11799 );
11800 assert_eq!(states.get(2), PieceState::Available);
11801 assert_eq!(states.get(3), PieceState::Available);
11802 }
11803
11804 #[test]
11813 fn dht_requery_guard_scales_with_max_peers() {
11814 assert_eq!(128_usize.saturating_mul(4), 512);
11816
11817 assert_eq!(200_usize.saturating_mul(4), 800);
11819
11820 assert_eq!(50_usize.saturating_mul(4), 200);
11822
11823 assert_eq!(usize::MAX.saturating_mul(4), usize::MAX);
11825 }
11826
11827 fn make_test_info_bytes() -> (Vec<u8>, Id20) {
11831 use serde::Serialize;
11832
11833 #[derive(Serialize)]
11834 struct Info<'a> {
11835 length: u64,
11836 name: &'a str,
11837 #[serde(rename = "piece length")]
11838 piece_length: u64,
11839 #[serde(with = "serde_bytes")]
11840 pieces: &'a [u8],
11841 }
11842
11843 let data = vec![0xAB; 1024];
11844 let piece_hash = irontide_core::sha1(&data);
11845 let mut pieces = Vec::new();
11846 pieces.extend_from_slice(piece_hash.as_bytes());
11847
11848 let info = Info {
11849 length: 1024,
11850 name: "test",
11851 piece_length: 16384,
11852 pieces: &pieces,
11853 };
11854
11855 let info_bytes = irontide_bencode::to_bytes(&info).unwrap();
11856 let info_hash = irontide_core::sha1(&info_bytes);
11857 (info_bytes, info_hash)
11858 }
11859
11860 async fn create_magnet_handle(info_hash: Id20) -> TorrentHandle {
11862 let magnet = Magnet {
11863 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
11864 display_name: Some("test".into()),
11865 trackers: vec![],
11866 peers: vec![],
11867 selected_files: None,
11868 };
11869 let config = test_config();
11870 let (atx, amask) = test_alert_channel();
11871 let (dm, _dj) = test_disk_manager();
11872 TorrentHandle::from_magnet(
11873 magnet,
11874 dm,
11875 config,
11876 test_dht_rx(),
11877 test_dht_rx(),
11878 None,
11879 None,
11880 crate::slot_tuner::SlotTuner::disabled(4),
11881 atx,
11882 amask,
11883 None,
11884 None,
11885 test_ban_manager(),
11886 test_ip_filter(),
11887 Arc::new(Vec::new()),
11888 None,
11889 None,
11890 Arc::new(crate::transport::NetworkFactory::tokio()),
11891 None,
11892 Arc::new(crate::stats::SessionCounters::new()),
11893 )
11894 .await
11895 .unwrap()
11896 }
11897
11898 #[tokio::test]
11899 async fn pre_resolved_metadata_applies_when_fetching() {
11900 let (info_bytes, info_hash) = make_test_info_bytes();
11901 let handle = create_magnet_handle(info_hash).await;
11902
11903 let stats = handle.stats().await.unwrap();
11905 assert_eq!(stats.state, TorrentState::FetchingMetadata);
11906
11907 let peer_addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
11909 handle.send_pre_resolved_metadata(info_bytes, vec![peer_addr]);
11910
11911 tokio::time::sleep(Duration::from_millis(200)).await;
11913
11914 let stats = handle.stats().await.unwrap();
11916 assert_eq!(
11917 stats.state,
11918 TorrentState::Downloading,
11919 "should have transitioned to Downloading after pre-resolved metadata"
11920 );
11921 assert!(
11922 stats.pieces_total > 0,
11923 "should know piece count after metadata resolution"
11924 );
11925
11926 handle.shutdown().await.unwrap();
11927 }
11928
11929 #[tokio::test]
11930 async fn pre_resolved_metadata_ignored_after_resolution() {
11931 let data = vec![0xAB; 32768];
11933 let meta = make_test_torrent(&data, 16384);
11934 let info_hash = meta.info_hash;
11935 let storage = make_storage(&data, 16384);
11936 let config = test_config();
11937
11938 let (atx, amask) = test_alert_channel();
11939 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
11940 let handle = TorrentHandle::from_torrent(
11941 meta,
11942 irontide_core::TorrentVersion::V1Only,
11943 None,
11944 dh,
11945 dm,
11946 config,
11947 test_dht_rx(),
11948 test_dht_rx(),
11949 None,
11950 None,
11951 crate::slot_tuner::SlotTuner::disabled(4),
11952 atx,
11953 amask,
11954 None,
11955 None,
11956 test_ban_manager(),
11957 test_ip_filter(),
11958 Arc::new(Vec::new()),
11959 None,
11960 None,
11961 Arc::new(crate::transport::NetworkFactory::tokio()),
11962 None,
11963 Arc::new(crate::stats::SessionCounters::new()),
11964 )
11965 .await
11966 .unwrap();
11967
11968 let stats_before = handle.stats().await.unwrap();
11969 assert_eq!(stats_before.state, TorrentState::Downloading);
11970
11971 let (info_bytes, _) = make_test_info_bytes();
11974 handle.send_pre_resolved_metadata(info_bytes, vec![]);
11975
11976 tokio::time::sleep(Duration::from_millis(100)).await;
11978
11979 let stats_after = handle.stats().await.unwrap();
11981 assert_eq!(stats_after.state, TorrentState::Downloading);
11982 assert_eq!(stats_after.pieces_total, stats_before.pieces_total);
11983
11984 handle.shutdown().await.unwrap();
11985 }
11986
11987 #[tokio::test]
11988 async fn pre_resolved_metadata_with_invalid_hash_stays_fetching() {
11989 let (info_bytes, _correct_hash) = make_test_info_bytes();
11993
11994 let wrong_hash = Id20::from_hex("0000000000000000000000000000000000000001").unwrap();
11996 let handle = create_magnet_handle(wrong_hash).await;
11997
11998 let stats = handle.stats().await.unwrap();
11999 assert_eq!(stats.state, TorrentState::FetchingMetadata);
12000
12001 handle.send_pre_resolved_metadata(info_bytes, vec![]);
12003
12004 tokio::time::sleep(Duration::from_millis(200)).await;
12005
12006 let stats = handle.stats().await.unwrap();
12008 assert_eq!(
12009 stats.state,
12010 TorrentState::FetchingMetadata,
12011 "should stay in FetchingMetadata when info_hash doesn't match"
12012 );
12013
12014 handle.shutdown().await.unwrap();
12015 }
12016
12017 #[test]
12018 fn initial_queue_depth_is_128() {
12019 use crate::peer_shared::INITIAL_QUEUE_DEPTH;
12020 assert_eq!(INITIAL_QUEUE_DEPTH, 128);
12021 }
12022
12023 #[tokio::test]
12036 #[allow(
12037 clippy::large_stack_arrays,
12038 reason = "test data buffer passed directly to make_storage"
12039 )]
12040 async fn m159_seed_mode_suppresses_new_requests_on_wire() {
12041 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let info_hash = meta.info_hash;
12044 let storage = make_storage(&[0u8; 32768], 16384);
12046
12047 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
12048 let listen_addr = listener.local_addr().unwrap();
12049 let config = TorrentConfig {
12050 listen_port: listen_addr.port(),
12051 ..test_config()
12052 };
12053 drop(listener);
12054
12055 let (atx, amask) = test_alert_channel();
12056 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
12057 let handle = TorrentHandle::from_torrent(
12058 meta,
12059 irontide_core::TorrentVersion::V1Only,
12060 None,
12061 dh,
12062 dm,
12063 config,
12064 test_dht_rx(),
12065 test_dht_rx(),
12066 None,
12067 None,
12068 crate::slot_tuner::SlotTuner::disabled(4),
12069 atx,
12070 amask,
12071 None,
12072 None,
12073 test_ban_manager(),
12074 test_ip_filter(),
12075 Arc::new(Vec::new()),
12076 None,
12077 None,
12078 Arc::new(crate::transport::NetworkFactory::tokio()),
12079 None,
12080 Arc::new(crate::stats::SessionCounters::new()),
12081 )
12082 .await
12083 .unwrap();
12084
12085 tokio::time::sleep(Duration::from_millis(50)).await;
12086
12087 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
12089 let (reader, writer) = tokio::io::split(stream);
12090 let mut writer = writer;
12091 let mut reader = reader;
12092
12093 let hs = Handshake::new(
12094 info_hash,
12095 Id20::from_hex("dddddddddddddddddddddddddddddddddddddddd").unwrap(),
12096 );
12097 writer.write_all(&hs.to_bytes()).await.unwrap();
12098 writer.flush().await.unwrap();
12099 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
12100 reader.read_exact(&mut hs_buf).await.unwrap();
12101
12102 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
12103 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
12104
12105 let _actor_ext_hs = framed_read.next().await;
12107 let ext_hs = ExtHandshake::new();
12108 let ext_payload = ext_hs.to_bytes().unwrap();
12109 framed_write
12110 .send(Message::Extended {
12111 ext_id: 0,
12112 payload: ext_payload,
12113 })
12114 .await
12115 .unwrap();
12116
12117 let mut bf = Bitfield::new(2);
12119 bf.set(0);
12120 bf.set(1);
12121 framed_write
12122 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
12123 .await
12124 .unwrap();
12125 framed_write.send(Message::Unchoke).await.unwrap();
12126
12127 let mut initial_request_seen = false;
12131 let wait_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12132 loop {
12133 let remaining = wait_deadline.saturating_duration_since(tokio::time::Instant::now());
12134 if remaining.is_zero() {
12135 break;
12136 }
12137 match tokio::time::timeout(remaining, framed_read.next()).await {
12138 Ok(Some(Ok(Message::Request { .. }))) => {
12139 initial_request_seen = true;
12140 break;
12141 }
12142 Ok(Some(Ok(_))) => {}
12143 _ => break,
12144 }
12145 }
12146 assert!(
12147 initial_request_seen,
12148 "actor should have sent a Request before seed mode toggle"
12149 );
12150
12151 handle.set_seed_mode(true).await.unwrap();
12154
12155 let grace_deadline = tokio::time::Instant::now() + Duration::from_millis(200);
12162 let mut cancel_seen = false;
12163 let mut grace_requests = 0u32;
12164 loop {
12165 let remaining = grace_deadline.saturating_duration_since(tokio::time::Instant::now());
12166 if remaining.is_zero() {
12167 break;
12168 }
12169 match tokio::time::timeout(remaining, framed_read.next()).await {
12170 Ok(Some(Ok(Message::Request { .. }))) => {
12171 grace_requests += 1;
12172 }
12173 Ok(Some(Ok(Message::Cancel { .. }))) => {
12174 cancel_seen = true;
12175 }
12176 Ok(Some(Ok(_))) => {}
12177 Ok(None | Some(Err(_))) | Err(_) => break,
12178 }
12179 }
12180 let _ = (cancel_seen, grace_requests);
12181
12182 let steady_deadline = tokio::time::Instant::now() + Duration::from_millis(500);
12185 let mut steady_requests = 0u32;
12186 loop {
12187 let remaining = steady_deadline.saturating_duration_since(tokio::time::Instant::now());
12188 if remaining.is_zero() {
12189 break;
12190 }
12191 match tokio::time::timeout(remaining, framed_read.next()).await {
12192 Ok(Some(Ok(Message::Request { .. }))) => {
12193 steady_requests += 1;
12194 }
12195 Ok(Some(Ok(_))) => {}
12196 Ok(None | Some(Err(_))) | Err(_) => break,
12197 }
12198 }
12199
12200 assert_eq!(
12201 steady_requests, 0,
12202 "after the Stop propagation grace window, no new Request messages \
12203 must appear during steady-state while user_seed_mode is active"
12204 );
12205
12206 let stats = handle.stats().await.unwrap();
12208 assert!(
12209 stats.user_seed_mode,
12210 "stats.user_seed_mode should be true after set_seed_mode(true)"
12211 );
12212
12213 handle.shutdown().await.unwrap();
12214 }
12215
12216 #[tokio::test]
12244 async fn m159_seed_mode_uploads_continue_on_wire() {
12245 const FILL_BYTE: u8 = 0x5A;
12246 const PIECE_LENGTH: u64 = 16384;
12247 const TOTAL_LEN: usize = 32768; let data = vec![FILL_BYTE; TOTAL_LEN];
12250 let meta = make_test_torrent(&data, PIECE_LENGTH);
12251 let info_hash = meta.info_hash;
12252 let storage = make_seeded_storage(&data, PIECE_LENGTH);
12254
12255 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
12256 let listen_addr = listener.local_addr().unwrap();
12257 let config = TorrentConfig {
12258 listen_port: listen_addr.port(),
12259 ..test_config()
12260 };
12261 drop(listener);
12262
12263 let (atx, amask) = test_alert_channel();
12264 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
12265 let handle = TorrentHandle::from_torrent(
12266 meta,
12267 irontide_core::TorrentVersion::V1Only,
12268 None,
12269 dh,
12270 dm,
12271 config,
12272 test_dht_rx(),
12273 test_dht_rx(),
12274 None,
12275 None,
12276 crate::slot_tuner::SlotTuner::disabled(4),
12277 atx,
12278 amask,
12279 None,
12280 None,
12281 test_ban_manager(),
12282 test_ip_filter(),
12283 Arc::new(Vec::new()),
12284 None,
12285 None,
12286 Arc::new(crate::transport::NetworkFactory::tokio()),
12287 None,
12288 Arc::new(crate::stats::SessionCounters::new()),
12289 )
12290 .await
12291 .unwrap();
12292
12293 let seeding_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12296 loop {
12297 tokio::time::sleep(Duration::from_millis(50)).await;
12298 let stats = handle.stats().await.unwrap();
12299 if stats.state == TorrentState::Seeding && stats.pieces_have == 2 {
12300 break;
12301 }
12302 if tokio::time::Instant::now() > seeding_deadline {
12303 let stats = handle.stats().await.unwrap();
12304 panic!(
12305 "actor did not reach Seeding state within 3s: state={:?}, have={}/{}",
12306 stats.state, stats.pieces_have, stats.pieces_total
12307 );
12308 }
12309 }
12310
12311 handle.set_seed_mode(true).await.unwrap();
12314 let stats = handle.stats().await.unwrap();
12315 assert!(
12316 stats.user_seed_mode,
12317 "stats.user_seed_mode should be true after set_seed_mode(true)"
12318 );
12319
12320 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
12322 let (reader, writer) = tokio::io::split(stream);
12323 let mut writer = writer;
12324 let mut reader = reader;
12325
12326 let hs = Handshake::new(
12327 info_hash,
12328 Id20::from_hex("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(),
12329 );
12330 writer.write_all(&hs.to_bytes()).await.unwrap();
12331 writer.flush().await.unwrap();
12332 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
12333 reader.read_exact(&mut hs_buf).await.unwrap();
12334
12335 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
12336 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
12337
12338 let _actor_ext_hs = framed_read.next().await;
12340 let ext_hs = ExtHandshake::new();
12341 let ext_payload = ext_hs.to_bytes().unwrap();
12342 framed_write
12343 .send(Message::Extended {
12344 ext_id: 0,
12345 payload: ext_payload,
12346 })
12347 .await
12348 .unwrap();
12349
12350 let bf = Bitfield::new(2);
12352 framed_write
12353 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
12354 .await
12355 .unwrap();
12356 framed_write.send(Message::Interested).await.unwrap();
12357
12358 let unchoke_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12362 let mut saw_unchoke = false;
12363 loop {
12364 let remaining = unchoke_deadline.saturating_duration_since(tokio::time::Instant::now());
12365 if remaining.is_zero() {
12366 break;
12367 }
12368 match tokio::time::timeout(remaining, framed_read.next()).await {
12369 Ok(Some(Ok(Message::Unchoke))) => {
12370 saw_unchoke = true;
12371 break;
12372 }
12373 Ok(Some(Ok(_))) => {}
12374 Ok(None | Some(Err(_))) => break,
12375 Err(_elapsed) => break,
12376 }
12377 }
12378 assert!(
12379 saw_unchoke,
12380 "actor should have unchoked the leecher while user_seed_mode is active"
12381 );
12382
12383 framed_write
12386 .send(Message::Request {
12387 index: 0,
12388 begin: 0,
12389 length: PIECE_LENGTH as u32,
12390 })
12391 .await
12392 .unwrap();
12393
12394 let piece_deadline = tokio::time::Instant::now() + Duration::from_secs(2);
12399 let mut got_piece = false;
12400 loop {
12401 let remaining = piece_deadline.saturating_duration_since(tokio::time::Instant::now());
12402 if remaining.is_zero() {
12403 break;
12404 }
12405 match tokio::time::timeout(remaining, framed_read.next()).await {
12406 Ok(Some(Ok(Message::Piece {
12407 index,
12408 begin,
12409 data_0,
12410 data_1,
12411 }))) => {
12412 assert_eq!(index, 0, "Piece index should match request");
12413 assert_eq!(begin, 0, "Piece begin should match request");
12414 let mut payload: Vec<u8> =
12415 Vec::with_capacity(data_0.len().saturating_add(data_1.len()));
12416 payload.extend_from_slice(&data_0);
12417 payload.extend_from_slice(&data_1);
12418 assert_eq!(
12419 payload.len(),
12420 PIECE_LENGTH as usize,
12421 "Piece payload length should match requested length"
12422 );
12423 assert!(
12424 payload.iter().all(|&b| b == FILL_BYTE),
12425 "Piece payload should contain the pre-seeded fill byte"
12426 );
12427 got_piece = true;
12428 break;
12429 }
12430 Ok(Some(Ok(_))) => {}
12431 Ok(None | Some(Err(_))) => break,
12432 Err(_elapsed) => break,
12433 }
12434 }
12435 assert!(
12436 got_piece,
12437 "actor should have served a Piece in response to Request while user_seed_mode is active"
12438 );
12439
12440 let stats = handle.stats().await.unwrap();
12443 assert!(
12444 stats.user_seed_mode,
12445 "stats.user_seed_mode should remain true after serving an upload"
12446 );
12447 assert!(
12448 stats.uploaded >= u64::from(PIECE_LENGTH as u32),
12449 "stats.uploaded should reflect the served block, got {}",
12450 stats.uploaded
12451 );
12452
12453 handle.shutdown().await.unwrap();
12454 }
12455
12456 #[tokio::test]
12459 async fn info_field_populated_for_torrent() {
12460 let data = vec![0xAB; 32768];
12461 let meta = make_test_torrent(&data, 16384);
12462 let storage = make_storage(&data, 16384);
12463 let config = test_config();
12464
12465 let (atx, amask) = test_alert_channel();
12466 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
12467 let handle = TorrentHandle::from_torrent(
12468 meta,
12469 irontide_core::TorrentVersion::V1Only,
12470 None,
12471 dh,
12472 dm,
12473 config,
12474 test_dht_rx(),
12475 test_dht_rx(),
12476 None,
12477 None,
12478 crate::slot_tuner::SlotTuner::disabled(4),
12479 atx,
12480 amask,
12481 None,
12482 None,
12483 test_ban_manager(),
12484 test_ip_filter(),
12485 Arc::new(Vec::new()),
12486 None,
12487 None,
12488 Arc::new(crate::transport::NetworkFactory::tokio()),
12489 None,
12490 Arc::new(crate::stats::SessionCounters::new()),
12491 )
12492 .await
12493 .unwrap();
12494
12495 tokio::time::sleep(Duration::from_millis(50)).await;
12496
12497 let rd = handle.save_resume_data().await.unwrap();
12498
12499 assert!(rd.info.is_some(), "rd.info should be Some for .torrent");
12501
12502 let info_bytes = rd.info.as_ref().unwrap();
12504 let info: irontide_core::InfoDict =
12505 irontide_bencode::from_bytes(info_bytes).expect("info bytes should deserialize");
12506 assert_eq!(info.name, "test");
12507 assert_eq!(info.piece_length, 16384);
12508
12509 handle.shutdown().await.unwrap();
12510 }
12511
12512 #[tokio::test]
12513 async fn info_hash2_none_for_v1_only() {
12514 let data = vec![0xCD; 16384];
12515 let meta = make_test_torrent(&data, 16384);
12516 let storage = make_storage(&data, 16384);
12517 let config = test_config();
12518
12519 let (atx, amask) = test_alert_channel();
12520 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
12521 let handle = TorrentHandle::from_torrent(
12522 meta,
12523 irontide_core::TorrentVersion::V1Only,
12524 None,
12525 dh,
12526 dm,
12527 config,
12528 test_dht_rx(),
12529 test_dht_rx(),
12530 None,
12531 None,
12532 crate::slot_tuner::SlotTuner::disabled(4),
12533 atx,
12534 amask,
12535 None,
12536 None,
12537 test_ban_manager(),
12538 test_ip_filter(),
12539 Arc::new(Vec::new()),
12540 None,
12541 None,
12542 Arc::new(crate::transport::NetworkFactory::tokio()),
12543 None,
12544 Arc::new(crate::stats::SessionCounters::new()),
12545 )
12546 .await
12547 .unwrap();
12548
12549 tokio::time::sleep(Duration::from_millis(50)).await;
12550
12551 let rd = handle.save_resume_data().await.unwrap();
12552
12553 assert!(
12555 rd.info_hash2.is_none(),
12556 "v1-only torrent should have info_hash2 = None"
12557 );
12558
12559 assert!(
12561 rd.added_time > 0,
12562 "added_time should be a positive POSIX timestamp"
12563 );
12564
12565 handle.shutdown().await.unwrap();
12566 }
12567
12568 #[tokio::test]
12569 async fn info_none_for_unresolved_magnet() {
12570 let magnet = Magnet {
12571 info_hashes: irontide_core::InfoHashes::v1_only(
12572 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
12573 ),
12574 display_name: Some("magnet-test".into()),
12575 trackers: vec![],
12576 peers: vec![],
12577 selected_files: None,
12578 };
12579 let config = test_config();
12580
12581 let (atx, amask) = test_alert_channel();
12582 let (dm, _dj) = test_disk_manager();
12583 let handle = TorrentHandle::from_magnet(
12584 magnet,
12585 dm,
12586 config,
12587 test_dht_rx(),
12588 test_dht_rx(),
12589 None,
12590 None,
12591 crate::slot_tuner::SlotTuner::disabled(4),
12592 atx,
12593 amask,
12594 None,
12595 None,
12596 test_ban_manager(),
12597 test_ip_filter(),
12598 Arc::new(Vec::new()),
12599 None,
12600 None,
12601 Arc::new(crate::transport::NetworkFactory::tokio()),
12602 None,
12603 Arc::new(crate::stats::SessionCounters::new()),
12604 )
12605 .await
12606 .unwrap();
12607
12608 tokio::time::sleep(Duration::from_millis(50)).await;
12609
12610 let rd = handle.save_resume_data().await.unwrap();
12611
12612 assert!(
12614 rd.info.is_none(),
12615 "unresolved magnet should have info = None"
12616 );
12617
12618 assert!(
12620 rd.added_time > 0,
12621 "added_time should be set for magnet links"
12622 );
12623
12624 handle.shutdown().await.unwrap();
12625 }
12626
12627 #[tokio::test]
12630 async fn torrent_command_get_meta_returns_none_before_metadata() {
12631 let info_hash =
12633 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").expect("valid hex");
12634 let handle = create_magnet_handle(info_hash).await;
12635
12636 let (tx, rx) = oneshot::channel();
12637 handle
12638 .cmd_tx
12639 .send(TorrentCommand::GetMeta { reply: tx })
12640 .await
12641 .expect("cmd_tx send");
12642 let result = rx.await.expect("GetMeta reply");
12643 assert!(
12644 result.is_none(),
12645 "pre-metadata magnet must return None from GetMeta"
12646 );
12647
12648 handle.shutdown().await.unwrap();
12649 }
12650
12651 #[tokio::test]
12652 async fn torrent_command_get_meta_returns_some_after_metadata() {
12653 let (info_bytes, info_hash) = make_test_info_bytes();
12656 let handle = create_magnet_handle(info_hash).await;
12657
12658 handle.send_pre_resolved_metadata(info_bytes, vec![]);
12659
12660 let mut result = None;
12664 for _ in 0..100 {
12665 tokio::time::sleep(Duration::from_millis(20)).await;
12666 let (tx, rx) = oneshot::channel();
12667 handle
12668 .cmd_tx
12669 .send(TorrentCommand::GetMeta { reply: tx })
12670 .await
12671 .expect("cmd_tx send");
12672 let r = rx.await.expect("GetMeta reply");
12673 if r.is_some() {
12674 result = r;
12675 break;
12676 }
12677 }
12678 let meta = result.expect("GetMeta must return Some after PreResolvedMetadata");
12679 assert_eq!(meta.info_hash, info_hash);
12680
12681 handle.shutdown().await.unwrap();
12682 }
12683
12684 #[tokio::test]
12687 async fn web_seed_progress_idle_to_active_on_first_success() {
12688 let mut actor = TorrentActor::for_throttle_test(8, 0);
12689 actor.handle_web_seed_progress("http://seed.example/file", 1024, 1_000_000, None);
12690 let stats = actor
12691 .web_seed_stats
12692 .get("http://seed.example/file")
12693 .expect("stats inserted");
12694 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12695 assert_eq!(stats.downloaded_bytes, 1024);
12696 assert_eq!(stats.last_rate_bps, 1_000_000);
12697 assert_eq!(stats.consecutive_failures, 0);
12698 assert!(stats.last_attempt_unix_secs > 0);
12699 assert!(actor.need_save_resume);
12700 }
12701
12702 #[tokio::test]
12703 async fn web_seed_progress_active_to_errored_then_recovery_persists_last_error() {
12704 let mut actor = TorrentActor::for_throttle_test(8, 0);
12705 let url = "http://seed.example/file".to_string();
12706
12707 actor.handle_web_seed_progress(&url, 1024, 100, None);
12709 assert_eq!(
12710 actor.web_seed_stats[&url].state,
12711 irontide_core::WebSeedState::Active
12712 );
12713
12714 actor.handle_web_seed_progress(&url, 1024, 0, Some("503".into()));
12716 let stats = &actor.web_seed_stats[&url];
12717 assert_eq!(stats.state, irontide_core::WebSeedState::Errored);
12718 assert_eq!(stats.last_error.as_deref(), Some("503"));
12719 assert_eq!(stats.consecutive_failures, 1);
12720
12721 actor.handle_web_seed_progress(&url, 2048, 200, None);
12723 let stats = &actor.web_seed_stats[&url];
12724 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12725 assert_eq!(
12726 stats.last_error.as_deref(),
12727 Some("503"),
12728 "last_error must persist through recovery (D-eng-8)"
12729 );
12730 assert_eq!(
12731 stats.consecutive_failures, 0,
12732 "consecutive_failures resets on success"
12733 );
12734 }
12735
12736 #[tokio::test]
12737 async fn web_seed_progress_consecutive_failures_monotonic_within_run() {
12738 let mut actor = TorrentActor::for_throttle_test(8, 0);
12739 let url = "http://seed.example/file".to_string();
12740
12741 actor.handle_web_seed_progress(&url, 0, 0, Some("e1".into()));
12742 actor.handle_web_seed_progress(&url, 0, 0, Some("e2".into()));
12743 actor.handle_web_seed_progress(&url, 0, 0, Some("e3".into()));
12744 let stats = &actor.web_seed_stats[&url];
12745 assert_eq!(stats.consecutive_failures, 3);
12746 assert_eq!(
12747 stats.last_error.as_deref(),
12748 Some("e3"),
12749 "last_error reflects most recent message"
12750 );
12751
12752 actor.handle_web_seed_progress(&url, 1024, 100, None);
12753 assert_eq!(
12754 actor.web_seed_stats[&url].consecutive_failures, 0,
12755 "success resets consecutive_failures"
12756 );
12757 }
12758
12759 fn install_peer_states(actor: &mut TorrentActor) {
12764 let (queue_tx, _queue_rx) = mpsc::unbounded_channel();
12765 actor.peer_states = Some(std::sync::Arc::new(crate::peer_states::PeerStates::new(
12766 queue_tx,
12767 )));
12768 }
12769
12770 fn addr(octet: u8, port: u16) -> std::net::SocketAddr {
12771 std::net::SocketAddr::new(
12772 std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 0, 2, octet)),
12773 port,
12774 )
12775 }
12776
12777 #[tokio::test]
12778 async fn pex_count_dedups_same_peer_in_two_messages() {
12779 let mut actor = TorrentActor::for_throttle_test(8, 0);
12780 install_peer_states(&mut actor);
12781
12782 actor.handle_add_peers(
12784 vec![addr(1, 6881), addr(2, 6881)],
12785 crate::peer_state::PeerSource::Pex,
12786 );
12787 actor.handle_add_peers(
12789 vec![addr(1, 6881), addr(3, 6881)],
12790 crate::peer_state::PeerSource::Pex,
12791 );
12792 assert_eq!(
12793 actor.pex_peer_count, 3,
12794 "3 unique peers across 2 PEX messages, A counted once"
12795 );
12796 assert_eq!(actor.lsd_peer_count, 0, "LSD untouched");
12797 }
12798
12799 #[tokio::test]
12800 async fn lsd_count_aggregates_across_multicasts() {
12801 let mut actor = TorrentActor::for_throttle_test(8, 0);
12802 install_peer_states(&mut actor);
12803
12804 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd);
12805 actor.handle_add_peers(
12806 vec![addr(2, 6881), addr(3, 6881)],
12807 crate::peer_state::PeerSource::Lsd,
12808 );
12809 actor.handle_add_peers(
12810 vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd,
12812 );
12813 assert_eq!(actor.lsd_peer_count, 3);
12814 }
12815
12816 #[tokio::test]
12817 async fn other_sources_do_not_bump_pex_or_lsd() {
12818 let mut actor = TorrentActor::for_throttle_test(8, 0);
12819 install_peer_states(&mut actor);
12820
12821 actor.handle_add_peers(
12822 vec![addr(1, 6881), addr(2, 6881)],
12823 crate::peer_state::PeerSource::Tracker,
12824 );
12825 actor.handle_add_peers(vec![addr(3, 6881)], crate::peer_state::PeerSource::Dht);
12826 actor.handle_add_peers(vec![addr(4, 6881)], crate::peer_state::PeerSource::Incoming);
12827 assert_eq!(actor.pex_peer_count, 0);
12828 assert_eq!(actor.lsd_peer_count, 0);
12829 }
12830
12831 #[tokio::test]
12832 async fn dedup_runs_against_global_seen_set() {
12833 let mut actor = TorrentActor::for_throttle_test(8, 0);
12839 install_peer_states(&mut actor);
12840
12841 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Tracker);
12842 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Pex);
12843 assert_eq!(
12844 actor.pex_peer_count, 0,
12845 "peer already seen via tracker — PEX shouldn't re-count"
12846 );
12847 }
12848
12849 #[tokio::test]
12850 async fn web_seed_progress_dirties_resume_flag() {
12851 let mut actor = TorrentActor::for_throttle_test(8, 0);
12852 actor.need_save_resume = false;
12853 actor.handle_web_seed_progress("http://x/file", 100, 50, None);
12854 assert!(
12855 actor.need_save_resume,
12856 "every progress event should mark fast-resume dirty"
12857 );
12858 }
12859
12860 #[tokio::test]
12861 async fn paused_torrent_rejects_outbound_peer_connect() {
12862 let mut actor = TorrentActor::for_throttle_test(8, 0);
12863 install_peer_states(&mut actor);
12864 actor.state = TorrentState::Paused;
12865
12866 let sem = Arc::new(tokio::sync::Semaphore::new(1));
12867 let permit = sem.clone().acquire_owned().await.unwrap();
12868 let connect = crate::peer_adder::ConnectPeer {
12869 addr: addr(1, 6881),
12870 source: crate::peer_state::PeerSource::Dht,
12871 permit,
12872 };
12873 actor.handle_adder_connect(connect);
12874 assert!(
12875 actor.peers.is_empty(),
12876 "paused torrent must not accept outbound peer connections"
12877 );
12878 assert_eq!(
12879 sem.available_permits(),
12880 1,
12881 "semaphore permit must be released on rejection"
12882 );
12883 }
12884
12885 #[tokio::test]
12886 async fn resume_from_queued_restores_fetching_metadata_for_magnets() {
12887 let mut actor = TorrentActor::for_throttle_test(0, 0);
12888 actor.state = TorrentState::Queued;
12889 assert!(
12890 actor.chunk_tracker.is_none(),
12891 "magnet torrent has no chunk tracker before metadata"
12892 );
12893 assert_eq!(actor.num_pieces, 0);
12894
12895 actor.handle_resume().await;
12896 assert_eq!(
12897 actor.state,
12898 TorrentState::FetchingMetadata,
12899 "magnet torrent must resume to FetchingMetadata, not Downloading"
12900 );
12901 }
12902
12903 #[tokio::test]
12904 async fn resume_from_queued_restores_downloading_when_metadata_known() {
12905 let mut actor = TorrentActor::for_throttle_test(8, 0);
12906 actor.state = TorrentState::Queued;
12907
12908 actor.handle_resume().await;
12909 assert_eq!(
12910 actor.state,
12911 TorrentState::Downloading,
12912 "torrent with known pieces must resume to Downloading"
12913 );
12914 }
12915
12916 #[tokio::test]
12917 async fn queued_torrent_rejects_outbound_peer_connect() {
12918 let mut actor = TorrentActor::for_throttle_test(8, 0);
12919 install_peer_states(&mut actor);
12920 actor.state = TorrentState::Queued;
12921
12922 let sem = Arc::new(tokio::sync::Semaphore::new(1));
12923 let permit = sem.clone().acquire_owned().await.unwrap();
12924 let connect = crate::peer_adder::ConnectPeer {
12925 addr: addr(1, 6881),
12926 source: crate::peer_state::PeerSource::Dht,
12927 permit,
12928 };
12929 actor.handle_adder_connect(connect);
12930 assert!(
12931 actor.peers.is_empty(),
12932 "queued torrent must not accept outbound peer connections"
12933 );
12934 assert_eq!(
12935 sem.available_permits(),
12936 1,
12937 "semaphore permit must be released on rejection"
12938 );
12939 }
12940
12941 fn inject_peer_for_flush(
12945 actor: &mut TorrentActor,
12946 peer_addr: std::net::SocketAddr,
12947 unchoke_started: Option<std::time::Instant>,
12948 prior_total: std::time::Duration,
12949 ) {
12950 let (cmd_tx, _cmd_rx) = mpsc::channel(8);
12951 let mut peer = crate::peer_state::PeerState::new(
12952 peer_addr,
12953 actor.num_pieces,
12954 cmd_tx,
12955 crate::peer_state::PeerSource::Tracker,
12956 Arc::new(AtomicU32::new(0)),
12957 Arc::new(AtomicU32::new(128)),
12958 Arc::new(tokio::sync::Notify::new()),
12959 );
12960 peer.am_unchoke_started_at = unchoke_started;
12961 peer.unchoke_duration_total = prior_total;
12962 actor.peers.insert(peer_addr, peer);
12963 }
12964
12965 #[tokio::test]
12966 async fn disconnect_while_unchoked_flushes_delta_into_torrent_map() {
12967 let mut actor = TorrentActor::for_throttle_test(8, 0);
12968 let p = addr(1, 6881);
12969
12970 inject_peer_for_flush(
12973 &mut actor,
12974 p,
12975 Some(std::time::Instant::now() - std::time::Duration::from_millis(50)),
12976 std::time::Duration::from_millis(100),
12977 );
12978
12979 actor.disconnect_peer(p, "test");
12980
12981 let total = actor
12982 .unchoke_durations
12983 .get(&p)
12984 .copied()
12985 .expect("disconnect must flush a non-zero delta into the torrent map");
12986 assert!(
12987 total >= std::time::Duration::from_millis(140),
12988 "expected ≥140 ms (100 prior + ~50 in-flight), got {total:?}"
12989 );
12990 }
12991
12992 #[tokio::test]
12993 async fn disconnect_then_reconnect_preserves_history() {
12994 let mut actor = TorrentActor::for_throttle_test(8, 0);
12995 let p = addr(2, 6881);
12996
12997 inject_peer_for_flush(&mut actor, p, None, std::time::Duration::from_millis(80));
12999 actor.disconnect_peer(p, "test");
13000 let after_first = *actor
13001 .unchoke_durations
13002 .get(&p)
13003 .expect("first flush must populate the entry");
13004 assert_eq!(after_first, std::time::Duration::from_millis(80));
13005
13006 inject_peer_for_flush(
13008 &mut actor,
13009 p,
13010 Some(std::time::Instant::now() - std::time::Duration::from_millis(40)),
13011 std::time::Duration::ZERO,
13012 );
13013 actor.disconnect_peer(p, "test");
13014 let after_second = *actor.unchoke_durations.get(&p).unwrap();
13015 assert!(
13016 after_second >= std::time::Duration::from_millis(120),
13017 "second flush must add to the existing entry, got {after_second:?}"
13018 );
13019 }
13020
13021 #[tokio::test]
13024 async fn piece_verified_wakes_reservation_notify() {
13025 let mut actor = TorrentActor::for_throttle_test(8, 0);
13026 let notify = Arc::new(tokio::sync::Notify::new());
13027 actor.reservation_notify = Some(Arc::clone(¬ify));
13028
13029 let notified = notify.notified();
13030 tokio::pin!(notified);
13031 assert!(
13032 futures::poll!(&mut notified).is_pending(),
13033 "notify should not have fired yet"
13034 );
13035
13036 actor.on_piece_verified(0).await;
13037
13038 tokio::time::timeout(Duration::from_secs(1), notified)
13039 .await
13040 .expect("reservation_notify must be woken by on_piece_verified");
13041 }
13042
13043 fn actor_with_tracker_state(queue: u32, inflight: u32) -> TorrentActor {
13049 use crate::piece_reservation::PieceTracker;
13050 use irontide_storage::Bitfield;
13051 let mut actor = TorrentActor::for_throttle_test(8, 0);
13052 let num_pieces = queue + inflight + 1;
13053 let we_have = Bitfield::new(num_pieces);
13054 let mut wanted = Bitfield::new(num_pieces);
13055 for i in 0..num_pieces {
13056 wanted.set(i);
13057 }
13058 let mut pt = PieceTracker::new(num_pieces, &we_have, &wanted);
13059 for i in queue..num_pieces {
13062 pt.mark_unwanted(i);
13063 }
13064 for i in 0..inflight {
13066 pt.record_reservation(i, "10.0.0.1:6881".parse().unwrap());
13067 }
13068 actor.piece_tracker = Some(pt);
13073 actor
13074 }
13075
13076 #[tokio::test]
13077 async fn pipeline_tick_skips_wake_when_dispatch_state_unchanged() {
13078 let mut actor = actor_with_tracker_state(10, 3);
13079 let notify = Arc::new(tokio::sync::Notify::new());
13080 actor.reservation_notify = Some(Arc::clone(¬ify));
13081
13082 actor.tick_dispatch_safety_wake();
13086 let _drain = notify.notified();
13087
13088 let notified = notify.notified();
13090 tokio::pin!(notified);
13091 actor.tick_dispatch_safety_wake();
13092
13093 tokio::task::yield_now().await;
13095 assert!(
13096 futures::poll!(&mut notified).is_pending(),
13097 "tick must not wake when (queue_count, inflight_count) is unchanged"
13098 );
13099 let skipped = actor.counters.get(crate::stats::DISPATCH_TICK_WAKE_SKIPPED);
13101 assert!(
13102 skipped >= 1,
13103 "expected DISPATCH_TICK_WAKE_SKIPPED >= 1, got {skipped}"
13104 );
13105 }
13106
13107 #[tokio::test]
13108 async fn pipeline_tick_wakes_when_inflight_changes() {
13109 let mut actor = actor_with_tracker_state(10, 3);
13110 let notify = Arc::new(tokio::sync::Notify::new());
13111 actor.reservation_notify = Some(Arc::clone(¬ify));
13112
13113 actor.tick_dispatch_safety_wake();
13115
13116 if let Some(ref mut pt) = actor.piece_tracker {
13119 pt.record_reservation(5, "10.0.0.2:6881".parse().unwrap());
13120 }
13121
13122 let notified = notify.notified();
13123 tokio::pin!(notified);
13124 actor.tick_dispatch_safety_wake();
13125
13126 tokio::time::timeout(Duration::from_secs(1), notified)
13127 .await
13128 .expect("tick must wake when dispatch state changed");
13129 }
13130}