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(crate) cmd_tx: mpsc::Sender<TorrentCommand>,
173}
174
175impl TorrentHandle {
176 #[allow(clippy::too_many_arguments)]
185 pub(crate) async fn from_torrent(
186 meta: TorrentMetaV1,
187 version: irontide_core::TorrentVersion,
188 meta_v2: Option<irontide_core::TorrentMetaV2>,
189 disk: DiskHandle,
190 disk_manager: DiskManagerHandle,
191 config: TorrentConfig,
192 dht_rx: irontide_dht::DhtReceiver,
193 dht_v6_rx: irontide_dht::DhtReceiver,
194 global_upload_bucket: Option<SharedBucket>,
195 global_download_bucket: Option<SharedBucket>,
196 slot_tuner: crate::slot_tuner::SlotTuner,
197 alert_tx: broadcast::Sender<Alert>,
198 alert_mask: Arc<AtomicU32>,
199 utp_socket: Option<irontide_utp::UtpSocket>,
200 utp_socket_v6: Option<irontide_utp::UtpSocket>,
201 ban_manager: crate::session::SharedBanManager,
202 ip_filter: crate::session::SharedIpFilter,
203 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
204 sam_session: Option<Arc<crate::i2p::SamSession>>,
205 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
206 factory: Arc<crate::transport::NetworkFactory>,
207 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
208 counters: Arc<crate::stats::SessionCounters>,
209 ) -> crate::Result<Self> {
210 let mut config = config;
211 if meta.info.private == Some(1) {
213 config.enable_dht = false;
214 config.enable_pex = false;
215 config.enable_lsd = false;
216 }
217
218 let info_hashes = match (&version, &meta_v2) {
219 (irontide_core::TorrentVersion::Hybrid, Some(v2_meta)) => {
220 if let Some(v2_hash) = v2_meta.info_hashes.v2 {
221 irontide_core::InfoHashes::hybrid(meta.info_hash, v2_hash)
222 } else {
223 irontide_core::InfoHashes::v1_only(meta.info_hash)
224 }
225 }
226 (irontide_core::TorrentVersion::V2Only, Some(v2_meta)) => v2_meta.info_hashes.clone(),
227 _ => irontide_core::InfoHashes::v1_only(meta.info_hash),
228 };
229
230 if meta.info.piece_length > config.max_piece_length {
231 return Err(crate::Error::InvalidSettings(format!(
232 "piece_length {} exceeds max_piece_length {}",
233 meta.info.piece_length, config.max_piece_length
234 )));
235 }
236
237 let num_pieces = meta.info.num_pieces() as u32;
238 let lengths = Lengths::new(
239 meta.info.total_length(),
240 meta.info.piece_length,
241 DEFAULT_CHUNK_SIZE,
242 );
243 let mut chunk_tracker = ChunkTracker::new(lengths.clone());
244
245 let hash_picker = if version.has_v2() {
247 if let Some(ref v2_meta) = meta_v2 {
248 chunk_tracker.enable_v2_tracking();
249
250 let block_size = 16384u64;
251 let blocks_per_piece = (meta.info.piece_length / block_size) as u32;
252
253 let v2_files = v2_meta.info.files();
255 let file_infos: Vec<irontide_core::FileHashInfo> = v2_files
256 .iter()
257 .filter_map(|f| {
258 let root = f.attr.pieces_root?;
259 let num_blocks = f.attr.length.div_ceil(block_size) as u32;
260 let num_pieces = f.attr.length.div_ceil(meta.info.piece_length) as u32;
261 Some(irontide_core::FileHashInfo {
262 root,
263 num_blocks,
264 num_pieces,
265 })
266 })
267 .collect();
268
269 if file_infos.is_empty() {
270 None
271 } else {
272 let mut picker = irontide_core::HashPicker::new(&file_infos, blocks_per_piece);
273
274 let _verified = picker.load_piece_layers(&v2_meta.piece_layers);
276
277 Some(picker)
278 }
279 } else {
280 None
281 }
282 } else {
283 None
284 };
285
286 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
287 let file_priorities = vec![FilePriority::Normal; file_lengths.len()];
288 let wanted_pieces =
289 crate::piece_selector::build_wanted_pieces(&file_priorities, &file_lengths, &lengths);
290
291 let (cmd_tx, cmd_rx) = mpsc::channel(256);
292 let (event_tx, event_rx) = mpsc::channel(2048);
293 let (write_error_tx, write_error_rx) = mpsc::channel(64);
294 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
295 let (hash_result_tx, hash_result_rx) = mpsc::channel(64); let our_peer_id = if config.anonymous_mode {
297 PeerId::generate_anonymous().0
298 } else {
299 PeerId::generate().0
300 };
301
302 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
305 .bind_tcp(SocketAddr::from((
306 std::net::Ipv6Addr::UNSPECIFIED,
307 config.listen_port,
308 )))
309 .await
310 {
311 Ok(l) => Some(l),
312 Err(_) => factory
313 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
314 .await
315 .ok(),
316 };
317 let mut tracker_manager = TrackerManager::from_torrent_filtered(
320 &meta,
321 our_peer_id,
322 config.listen_port,
323 config.url_security,
324 config.peer_dscp,
325 config.anonymous_mode,
326 );
327 tracker_manager.set_info_hashes(info_hashes.clone());
328
329 if let Some(ref sam) = sam_session {
331 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
332 }
333
334 let enable_dht = config.enable_dht;
335
336 let dht_initial = dht_rx.current();
341 let dht_v6_initial = dht_v6_rx.current();
342
343 let dht_peers_rx = if enable_dht {
345 if let Some(ref dht) = dht_initial {
346 match dht.get_peers(meta.info_hash).await {
347 Ok(rx) => Some(rx),
348 Err(e) => {
349 warn!("failed to start DHT v4 get_peers: {e}");
350 None
351 }
352 }
353 } else {
354 None
355 }
356 } else {
357 None
358 };
359
360 let dht_v6_peers_rx = if enable_dht {
361 if let Some(ref dht6) = dht_v6_initial {
362 match dht6.get_peers(meta.info_hash).await {
363 Ok(rx) => Some(rx),
364 Err(e) => {
365 debug!("failed to start DHT v6 get_peers: {e}");
366 None
367 }
368 }
369 } else {
370 None
371 }
372 } else {
373 None
374 };
375
376 let v2_as_v1 = if info_hashes.is_hybrid() {
378 info_hashes
379 .v2
380 .map(|v2| Id20(v2.0[..20].try_into().unwrap()))
381 } else {
382 None
383 };
384 let (dht_v2_peers_rx, dht_v6_v2_peers_rx) =
385 if let (true, Some(v2_id)) = (enable_dht, v2_as_v1) {
386 let rx4 = if let Some(ref dht) = dht_initial {
387 dht.get_peers(v2_id).await.ok()
388 } else {
389 None
390 };
391 let rx6 = if let Some(ref dht6) = dht_v6_initial {
392 dht6.get_peers(v2_id).await.ok()
393 } else {
394 None
395 };
396 (rx4, rx6)
397 } else {
398 (None, None)
399 };
400
401 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
402 let download_bucket = Arc::new(parking_lot::Mutex::new(
403 crate::rate_limiter::TokenBucket::new(config.download_rate_limit),
404 ));
405 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
406 0,
407 0,
408 0,
409 0,
410 config.upload_rate_limit,
411 config.download_rate_limit,
412 );
413
414 let super_seed = if config.super_seeding {
415 Some(crate::super_seed::SuperSeedState::new())
416 } else {
417 None
418 };
419 let (have_broadcast_tx, _) =
421 tokio::sync::broadcast::channel(std::cmp::max(128, num_pieces as usize / 4));
422 let is_share_mode = config.share_mode;
423
424 let (piece_ready_tx, _) = broadcast::channel(64);
425 let initial_have = chunk_tracker.bitfield().clone();
426 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(initial_have);
427 let stream_read_semaphore =
428 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
429
430 let choker = Choker::with_algorithms(
431 initial_unchoke_slots(config.max_uploads_per_torrent),
432 config.seed_choking_algorithm,
433 config.choking_algorithm,
434 config.upload_rate_limit,
435 2,
436 20,
437 );
438
439 let mut disk = disk;
441 if matches!(version, irontide_core::TorrentVersion::V1Only)
442 && let Some(pool) = &hash_pool
443 {
444 disk.set_hash_pool(pool.clone());
445 disk.set_hash_result_tx(hash_result_tx.clone());
446 }
447
448 let cached_files = Some(build_cached_file_info(&meta, &lengths));
450
451 let (order_map_tx, _order_map_rx_seed) =
453 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
454
455 let actor = TorrentActor {
456 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
457 config.lock_warn_threshold_ms,
458 ),
459 config,
460 info_hash: meta.info_hash,
461 our_peer_id,
462 state: TorrentState::Downloading,
463 disk: Some(disk),
464 disk_manager,
465 chunk_tracker: Some(chunk_tracker),
466 lengths: Some(lengths),
467 num_pieces,
468 streaming_pieces: BTreeSet::new(),
469 time_critical_pieces: BTreeSet::new(),
470 streaming_cursors: Vec::new(),
471 piece_ready_tx,
472 have_watch_tx,
473 have_watch_rx,
474 stream_read_semaphore,
475 file_priorities,
476 wanted_pieces,
477 end_game: EndGame::new(),
478 peers: HashMap::new(),
479 unchoke_durations: HashMap::new(),
480 cached_peer_rates: FxHashMap::default(),
481 refill_notify: Arc::new(tokio::sync::Notify::new()),
482 atomic_states: None,
483 block_maps: None,
484 steal_candidates: None,
485 last_steal_populate: Instant::now(),
486 piece_write_guards: None,
487 soft_reap_buf: Vec::new(),
488 eviction_history: std::collections::VecDeque::new(),
489 force_immediate_choker_tick: false,
490 piece_tracker: None,
491 order_map_tx,
492 piece_owner: Vec::new(),
493 peer_slab: crate::piece_reservation::PeerSlab::new(),
494 priority_pieces: BTreeSet::new(),
495 max_in_flight: 512,
496 reservation_notify: None,
497 last_tick_dispatch_state: None,
498 choker,
499 user_seed_mode: false,
500 user_forced: false,
501 max_connections: 0,
502 peer_states: None,
503 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
504 connect_permits: HashMap::new(),
505 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
506 std::collections::HashMap::new(),
507 )),
508 connect_rx: None,
509 metadata_downloader: None,
510 downloaded: 0,
511 uploaded: 0,
512 checking_progress: 0.0,
513 total_download: 0,
514 total_upload: 0,
515 total_failed_bytes: 0,
516 total_redundant_bytes: 0,
517 added_time: std::time::SystemTime::now()
518 .duration_since(std::time::UNIX_EPOCH)
519 .map_or(0, |d| d.as_secs() as i64),
520 completed_time: 0,
521 last_download: 0,
522 last_upload: 0,
523 last_seen_complete: 0,
524 active_duration: 0,
525 finished_duration: 0,
526 seeding_duration: 0,
527 active_since: Some(std::time::Instant::now()),
528 state_duration_since: None,
529 started_at: std::time::Instant::now(),
530 moving_storage: false,
531 has_incoming: false,
532 need_save_resume: false,
533 error: String::new(),
534 error_file: -1,
535 cmd_rx,
536 event_tx,
537 event_rx,
538 write_error_rx,
539 write_error_tx,
540 verify_result_rx,
541 verify_result_tx,
542 pending_verify: HashSet::new(),
543 piece_generations: vec![0u64; num_pieces as usize],
544 hash_result_rx,
545 hash_result_tx,
546 meta: Some(meta),
547 cached_files,
548 listener,
549 utp_socket,
550 utp_socket_v6,
551 tracker_manager,
552 tracker_result_rx: None,
553 dht_rx,
554 dht_v6_rx,
555 dht_enabled: enable_dht,
556 dht_peers_rx,
557 dht_v6_peers_rx,
558 dht_v6_empty_count: 0,
559 dht_v6_last_retry: None,
560 alert_tx,
561 alert_mask,
562 upload_bucket,
563 download_bucket,
564 global_upload_bucket,
565 global_download_bucket,
566 slot_tuner,
567 upload_bytes_interval: 0,
568 peak_download_rate: 0,
569 web_seeds: HashMap::new(),
570 banned_web_seeds: HashSet::new(),
571 web_seed_in_flight: HashMap::new(),
572 web_seed_stats: HashMap::new(),
573 pex_peer_count: 0,
574 lsd_peer_count: 0,
575 super_seed,
576 have_broadcast_tx,
577 suggested_to_peers: HashMap::new(),
578 predictive_have_sent: HashSet::new(),
579
580 ban_manager,
581 ip_filter,
582 piece_contributors: HashMap::new(),
583 parole_pieces: HashMap::new(),
584 external_ip: None,
585 share_lru: std::collections::VecDeque::new(),
586 share_max_pieces: if is_share_mode { 64 } else { 0 },
587 plugins,
588 hash_picker,
589 version,
590 meta_v2,
591 info_hashes,
592 dht_v2_peers_rx,
593 dht_v6_v2_peers_rx,
594 magnet_selected_files: None,
595 sam_session,
596 i2p_accept_rx: None,
597 i2p_peer_counter: 0,
598 i2p_destinations: HashMap::new(),
599 ssl_manager,
600 rate_limiter_set,
601 auto_sequential_active: false,
602 factory,
603 hash_pool_ref: hash_pool,
604 connect_attempts: 0,
605 connect_failures: 0,
606 choke_rotations: 0,
607 inflight_started: Vec::new(),
608 completed_piece_times: std::collections::VecDeque::new(),
609 piece_steals: 0,
610 holepunch_relayed: 0,
611 holepunch_relay_rate: HashMap::new(),
612 holepunch_cooldowns: HashMap::new(),
613 holepunch_pending: Vec::new(),
614 counters,
615 };
616
617 let spawn_info_hash = actor.info_hash;
618 let join_handle = tokio::spawn(actor.run());
619 tokio::spawn(async move {
621 match join_handle.await {
622 Ok(()) => {
623 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
624 }
625 Err(e) if e.is_panic() => {
626 let panic_payload = e.into_panic();
627 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
628 (*s).to_string()
629 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
630 s.clone()
631 } else {
632 "unknown panic payload".to_string()
633 };
634 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
635 }
636 Err(e) => {
637 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
638 }
639 }
640 });
641 Ok(Self { cmd_tx })
642 }
643
644 #[allow(clippy::too_many_arguments)]
650 pub(crate) async fn from_magnet(
651 magnet: Magnet,
652 disk_manager: DiskManagerHandle,
653 config: TorrentConfig,
654 dht_rx: irontide_dht::DhtReceiver,
655 dht_v6_rx: irontide_dht::DhtReceiver,
656 global_upload_bucket: Option<SharedBucket>,
657 global_download_bucket: Option<SharedBucket>,
658 slot_tuner: crate::slot_tuner::SlotTuner,
659 alert_tx: broadcast::Sender<Alert>,
660 alert_mask: Arc<AtomicU32>,
661 utp_socket: Option<irontide_utp::UtpSocket>,
662 utp_socket_v6: Option<irontide_utp::UtpSocket>,
663 ban_manager: crate::session::SharedBanManager,
664 ip_filter: crate::session::SharedIpFilter,
665 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
666 sam_session: Option<Arc<crate::i2p::SamSession>>,
667 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
668 factory: Arc<crate::transport::NetworkFactory>,
669 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
670 counters: Arc<crate::stats::SessionCounters>,
671 ) -> crate::Result<Self> {
672 let (cmd_tx, cmd_rx) = mpsc::channel(256);
673 let (event_tx, event_rx) = mpsc::channel(2048);
674 let (write_error_tx, write_error_rx) = mpsc::channel(64);
675 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
676 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
678 let our_peer_id = if config.anonymous_mode {
679 PeerId::generate_anonymous().0
680 } else {
681 PeerId::generate().0
682 };
683
684 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
686 .bind_tcp(SocketAddr::from((
687 std::net::Ipv6Addr::UNSPECIFIED,
688 config.listen_port,
689 )))
690 .await
691 {
692 Ok(l) => Some(l),
693 Err(_) => factory
694 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
695 .await
696 .ok(),
697 };
698 let mut tracker_manager = TrackerManager::empty(
701 magnet.info_hash(),
702 our_peer_id,
703 config.listen_port,
704 config.peer_dscp,
705 config.anonymous_mode,
706 );
707 for url in &magnet.trackers {
709 tracker_manager.add_tracker_url(url);
710 }
711
712 if let Some(ref sam) = sam_session {
714 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
715 }
716
717 let enable_dht = config.enable_dht;
718
719 let dht_initial = dht_rx.current();
723 let dht_v6_initial = dht_v6_rx.current();
724
725 let dht_peers_rx = if enable_dht {
727 if let Some(ref dht) = dht_initial {
728 match dht.get_peers(magnet.info_hash()).await {
729 Ok(rx) => Some(rx),
730 Err(e) => {
731 warn!("failed to start DHT v4 get_peers: {e}");
732 None
733 }
734 }
735 } else {
736 None
737 }
738 } else {
739 None
740 };
741
742 let dht_v6_peers_rx = if enable_dht {
743 if let Some(ref dht6) = dht_v6_initial {
744 match dht6.get_peers(magnet.info_hash()).await {
745 Ok(rx) => Some(rx),
746 Err(e) => {
747 debug!("failed to start DHT v6 get_peers: {e}");
748 None
749 }
750 }
751 } else {
752 None
753 }
754 } else {
755 None
756 };
757
758 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
759 let download_bucket = Arc::new(parking_lot::Mutex::new(
760 crate::rate_limiter::TokenBucket::new(config.download_rate_limit),
761 ));
762 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
763 0,
764 0,
765 0,
766 0,
767 config.upload_rate_limit,
768 config.download_rate_limit,
769 );
770
771 let super_seed = if config.super_seeding {
772 Some(crate::super_seed::SuperSeedState::new())
773 } else {
774 None
775 };
776 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
778 let is_share_mode = config.share_mode;
779 let magnet_selected_files = magnet.selected_files.clone();
780 let info_hashes = magnet.info_hashes.clone();
781
782 let (piece_ready_tx, _) = broadcast::channel(64);
783 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(0));
784 let stream_read_semaphore =
785 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
786
787 let choker = Choker::with_algorithms(
788 initial_unchoke_slots(config.max_uploads_per_torrent),
789 config.seed_choking_algorithm,
790 config.choking_algorithm,
791 config.upload_rate_limit,
792 2,
793 20,
794 );
795
796 let (order_map_tx, _order_map_rx_seed) =
797 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
798
799 let actor = TorrentActor {
800 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
801 config.lock_warn_threshold_ms,
802 ),
803 config,
804 info_hash: magnet.info_hash(),
805 our_peer_id,
806 state: TorrentState::FetchingMetadata,
807 disk: None,
808 disk_manager,
809 chunk_tracker: None,
810 lengths: None,
811 num_pieces: 0,
812 streaming_pieces: BTreeSet::new(),
813 time_critical_pieces: BTreeSet::new(),
814 streaming_cursors: Vec::new(),
815 piece_ready_tx,
816 have_watch_tx,
817 have_watch_rx,
818 stream_read_semaphore,
819 file_priorities: Vec::new(),
820 wanted_pieces: Bitfield::new(0),
821 end_game: EndGame::new(),
822 peers: HashMap::new(),
823 unchoke_durations: HashMap::new(),
824 cached_peer_rates: FxHashMap::default(),
825 refill_notify: Arc::new(tokio::sync::Notify::new()),
826 atomic_states: None,
827 block_maps: None,
828 steal_candidates: None,
829 last_steal_populate: Instant::now(),
830 piece_write_guards: None,
831 soft_reap_buf: Vec::new(),
832 eviction_history: std::collections::VecDeque::new(),
833 force_immediate_choker_tick: false,
834 piece_tracker: None,
835 order_map_tx,
836 piece_owner: Vec::new(),
837 peer_slab: crate::piece_reservation::PeerSlab::new(),
838 priority_pieces: BTreeSet::new(),
839 max_in_flight: 512,
840 reservation_notify: None,
841 last_tick_dispatch_state: None,
842 choker,
843 user_seed_mode: false,
844 user_forced: false,
845 max_connections: 0,
846 peer_states: None,
847 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
848 connect_permits: HashMap::new(),
849 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
850 std::collections::HashMap::new(),
851 )),
852 connect_rx: None,
853 metadata_downloader: Some(MetadataDownloader::new(magnet.info_hash())),
854 downloaded: 0,
855 uploaded: 0,
856 checking_progress: 0.0,
857 total_download: 0,
858 total_upload: 0,
859 total_failed_bytes: 0,
860 total_redundant_bytes: 0,
861 added_time: std::time::SystemTime::now()
862 .duration_since(std::time::UNIX_EPOCH)
863 .map_or(0, |d| d.as_secs() as i64),
864 completed_time: 0,
865 last_download: 0,
866 last_upload: 0,
867 last_seen_complete: 0,
868 active_duration: 0,
869 finished_duration: 0,
870 seeding_duration: 0,
871 active_since: Some(std::time::Instant::now()),
872 state_duration_since: None,
873 started_at: std::time::Instant::now(),
874 moving_storage: false,
875 has_incoming: false,
876 need_save_resume: false,
877 error: String::new(),
878 error_file: -1,
879 cmd_rx,
880 event_tx,
881 event_rx,
882 write_error_rx,
883 write_error_tx,
884 verify_result_rx,
885 verify_result_tx,
886 pending_verify: HashSet::new(),
887 piece_generations: Vec::new(),
888 hash_result_rx,
889 hash_result_tx,
890 meta: None,
891 cached_files: None,
892 listener,
893 utp_socket,
894 utp_socket_v6,
895 tracker_manager,
896 tracker_result_rx: None,
897 dht_rx,
898 dht_v6_rx,
899 dht_enabled: enable_dht,
900 dht_peers_rx,
901 dht_v6_peers_rx,
902 dht_v6_empty_count: 0,
903 dht_v6_last_retry: None,
904 alert_tx,
905 alert_mask,
906 upload_bucket,
907 download_bucket,
908 global_upload_bucket,
909 global_download_bucket,
910 slot_tuner,
911 upload_bytes_interval: 0,
912 peak_download_rate: 0,
913 web_seeds: HashMap::new(),
914 banned_web_seeds: HashSet::new(),
915 web_seed_in_flight: HashMap::new(),
916 web_seed_stats: HashMap::new(),
917 pex_peer_count: 0,
918 lsd_peer_count: 0,
919 super_seed,
920 have_broadcast_tx,
921 suggested_to_peers: HashMap::new(),
922 predictive_have_sent: HashSet::new(),
923
924 ban_manager,
925 ip_filter,
926 piece_contributors: HashMap::new(),
927 parole_pieces: HashMap::new(),
928 external_ip: None,
929 share_lru: std::collections::VecDeque::new(),
930 share_max_pieces: if is_share_mode { 64 } else { 0 },
931 plugins,
932 hash_picker: None,
933 version: irontide_core::TorrentVersion::V1Only,
934 meta_v2: None,
935 info_hashes,
936 dht_v2_peers_rx: None,
937 dht_v6_v2_peers_rx: None,
938 magnet_selected_files,
939 sam_session,
940 i2p_accept_rx: None,
941 i2p_peer_counter: 0,
942 i2p_destinations: HashMap::new(),
943 ssl_manager,
944 rate_limiter_set,
945 auto_sequential_active: false,
946 factory,
947 hash_pool_ref: hash_pool,
948 connect_attempts: 0,
949 connect_failures: 0,
950 choke_rotations: 0,
951 inflight_started: Vec::new(),
952 completed_piece_times: std::collections::VecDeque::new(),
953 piece_steals: 0,
954 holepunch_relayed: 0,
955 holepunch_relay_rate: HashMap::new(),
956 holepunch_cooldowns: HashMap::new(),
957 holepunch_pending: Vec::new(),
958 counters,
959 };
960
961 let spawn_info_hash = actor.info_hash;
962 let join_handle = tokio::spawn(actor.run());
963 tokio::spawn(async move {
964 match join_handle.await {
965 Ok(()) => {
966 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
967 }
968 Err(e) if e.is_panic() => {
969 let panic_payload = e.into_panic();
970 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
971 (*s).to_string()
972 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
973 s.clone()
974 } else {
975 "unknown panic payload".to_string()
976 };
977 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
978 }
979 Err(e) => {
980 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
981 }
982 }
983 });
984 Ok(Self { cmd_tx })
985 }
986
987 pub(crate) async fn send_incoming_peer(
989 &self,
990 stream: crate::transport::BoxedStream,
991 addr: SocketAddr,
992 ) -> crate::Result<()> {
993 self.cmd_tx
994 .send(TorrentCommand::IncomingPeer { stream, addr })
995 .await
996 .map_err(|_| crate::Error::Shutdown)
997 }
998
999 pub async fn stats(&self) -> crate::Result<TorrentStats> {
1005 let (tx, rx) = oneshot::channel();
1006 self.cmd_tx
1007 .send(TorrentCommand::Stats { reply: tx })
1008 .await
1009 .map_err(|_| crate::Error::Shutdown)?;
1010 rx.await.map_err(|_| crate::Error::Shutdown)
1011 }
1012
1013 pub async fn get_meta(&self) -> crate::Result<Option<TorrentMetaV1>> {
1029 let (tx, rx) = oneshot::channel();
1030 self.cmd_tx
1031 .send(TorrentCommand::GetMeta { reply: tx })
1032 .await
1033 .map_err(|_| crate::Error::Shutdown)?;
1034 rx.await.map_err(|_| crate::Error::Shutdown)
1035 }
1036
1037 pub async fn add_peers(&self, peers: Vec<SocketAddr>, source: PeerSource) -> crate::Result<()> {
1043 self.cmd_tx
1044 .send(TorrentCommand::AddPeers { peers, source })
1045 .await
1046 .map_err(|_| crate::Error::Shutdown)
1047 }
1048
1049 pub async fn pause(&self) -> crate::Result<()> {
1055 self.cmd_tx
1056 .send(TorrentCommand::Pause)
1057 .await
1058 .map_err(|_| crate::Error::Shutdown)
1059 }
1060
1061 pub async fn queue(&self) -> crate::Result<()> {
1067 self.cmd_tx
1068 .send(TorrentCommand::Queue)
1069 .await
1070 .map_err(|_| crate::Error::Shutdown)
1071 }
1072
1073 pub async fn set_category(&self, category: Option<String>) -> crate::Result<()> {
1082 let (tx, rx) = oneshot::channel();
1083 self.cmd_tx
1084 .send(TorrentCommand::SetCategory {
1085 category,
1086 reply: tx,
1087 })
1088 .await
1089 .map_err(|_| crate::Error::Shutdown)?;
1090 rx.await.map_err(|_| crate::Error::Shutdown)
1091 }
1092
1093 pub async fn set_tags(&self, tags: Vec<String>) -> crate::Result<()> {
1104 let (tx, rx) = oneshot::channel();
1105 self.cmd_tx
1106 .send(TorrentCommand::SetTags { tags, reply: tx })
1107 .await
1108 .map_err(|_| crate::Error::Shutdown)?;
1109 rx.await.map_err(|_| crate::Error::Shutdown)
1110 }
1111
1112 pub async fn resume(&self) -> crate::Result<()> {
1118 self.cmd_tx
1119 .send(TorrentCommand::Resume)
1120 .await
1121 .map_err(|_| crate::Error::Shutdown)
1122 }
1123
1124 pub async fn shutdown(&self) -> crate::Result<()> {
1130 let _ = tokio::time::timeout(
1133 std::time::Duration::from_secs(5),
1134 self.cmd_tx.send(TorrentCommand::Shutdown),
1135 )
1136 .await;
1137 Ok(())
1138 }
1139
1140 pub async fn save_resume_data(&self) -> crate::Result<irontide_core::FastResumeData> {
1146 let (tx, rx) = oneshot::channel();
1147 self.cmd_tx
1148 .send(TorrentCommand::SaveResumeData { reply: tx })
1149 .await
1150 .map_err(|_| crate::Error::Shutdown)?;
1151 rx.await.map_err(|_| crate::Error::Shutdown)?
1152 }
1153
1154 pub(crate) async fn clear_save_resume_flag(&self) -> crate::Result<()> {
1156 self.cmd_tx
1157 .send(TorrentCommand::ClearSaveResumeFlag)
1158 .await
1159 .map_err(|_| crate::Error::Shutdown)
1160 }
1161
1162 pub(crate) async fn restore_resume_bitmap(&self, pieces: Vec<u8>) -> crate::Result<()> {
1173 let (tx, rx) = oneshot::channel();
1174 self.cmd_tx
1175 .send(TorrentCommand::RestoreResumeBitmap { pieces, reply: tx })
1176 .await
1177 .map_err(|_| crate::Error::Shutdown)?;
1178 rx.await.map_err(|_| crate::Error::Shutdown)?
1179 }
1180
1181 pub(crate) async fn restore_web_seed_stats(
1187 &self,
1188 stats: HashMap<String, irontide_core::WebSeedStats>,
1189 ) -> crate::Result<()> {
1190 let (tx, rx) = oneshot::channel();
1191 self.cmd_tx
1192 .send(TorrentCommand::RestoreWebSeedStats { stats, reply: tx })
1193 .await
1194 .map_err(|_| crate::Error::Shutdown)?;
1195 rx.await.map_err(|_| crate::Error::Shutdown)?
1196 }
1197
1198 pub(crate) async fn peer_source_counts(&self) -> crate::Result<(usize, usize)> {
1206 let (tx, rx) = oneshot::channel();
1207 self.cmd_tx
1208 .send(TorrentCommand::GetPeerSourceCounts { reply: tx })
1209 .await
1210 .map_err(|_| crate::Error::Shutdown)?;
1211 rx.await.map_err(|_| crate::Error::Shutdown)
1212 }
1213
1214 pub(crate) async fn query_unchoke_durations(
1220 &self,
1221 ) -> crate::Result<HashMap<SocketAddr, std::time::Duration>> {
1222 let (tx, rx) = oneshot::channel();
1223 self.cmd_tx
1224 .send(TorrentCommand::QueryUnchokeDurations { reply: tx })
1225 .await
1226 .map_err(|_| crate::Error::Shutdown)?;
1227 rx.await.map_err(|_| crate::Error::Shutdown)
1228 }
1229
1230 pub(crate) async fn get_web_seed_stats(
1236 &self,
1237 ) -> crate::Result<Vec<irontide_core::WebSeedStats>> {
1238 let (tx, rx) = oneshot::channel();
1239 self.cmd_tx
1240 .send(TorrentCommand::GetWebSeedStats { reply: tx })
1241 .await
1242 .map_err(|_| crate::Error::Shutdown)?;
1243 rx.await.map_err(|_| crate::Error::Shutdown)
1244 }
1245
1246 pub async fn set_file_priority(
1252 &self,
1253 index: usize,
1254 priority: irontide_core::FilePriority,
1255 ) -> crate::Result<()> {
1256 let (tx, rx) = oneshot::channel();
1257 self.cmd_tx
1258 .send(TorrentCommand::SetFilePriority {
1259 index,
1260 priority,
1261 reply: tx,
1262 })
1263 .await
1264 .map_err(|_| crate::Error::Shutdown)?;
1265 rx.await.map_err(|_| crate::Error::Shutdown)?
1266 }
1267
1268 pub async fn file_priorities(&self) -> crate::Result<Vec<irontide_core::FilePriority>> {
1274 let (tx, rx) = oneshot::channel();
1275 self.cmd_tx
1276 .send(TorrentCommand::FilePriorities { reply: tx })
1277 .await
1278 .map_err(|_| crate::Error::Shutdown)?;
1279 rx.await.map_err(|_| crate::Error::Shutdown)
1280 }
1281
1282 pub async fn tracker_list(&self) -> crate::Result<Vec<crate::tracker_manager::TrackerInfo>> {
1288 let (tx, rx) = oneshot::channel();
1289 self.cmd_tx
1290 .send(TorrentCommand::TrackerList { reply: tx })
1291 .await
1292 .map_err(|_| crate::Error::Shutdown)?;
1293 rx.await.map_err(|_| crate::Error::Shutdown)
1294 }
1295
1296 pub async fn get_web_seeds(&self) -> crate::Result<Vec<String>> {
1302 let (tx, rx) = oneshot::channel();
1303 self.cmd_tx
1304 .send(TorrentCommand::GetWebSeeds { reply: tx })
1305 .await
1306 .map_err(|_| crate::Error::Shutdown)?;
1307 rx.await.map_err(|_| crate::Error::Shutdown)
1308 }
1309
1310 pub async fn get_piece_states(&self) -> crate::Result<Vec<u8>> {
1316 let (tx, rx) = oneshot::channel();
1317 self.cmd_tx
1318 .send(TorrentCommand::GetPieceStates { reply: tx })
1319 .await
1320 .map_err(|_| crate::Error::Shutdown)?;
1321 rx.await.map_err(|_| crate::Error::Shutdown)
1322 }
1323
1324 pub async fn get_piece_hashes(&self, offset: u32, limit: u32) -> crate::Result<Vec<String>> {
1334 let (tx, rx) = oneshot::channel();
1335 self.cmd_tx
1336 .send(TorrentCommand::GetPieceHashes {
1337 offset,
1338 limit,
1339 reply: tx,
1340 })
1341 .await
1342 .map_err(|_| crate::Error::Shutdown)?;
1343 rx.await.map_err(|_| crate::Error::Shutdown)
1344 }
1345
1346 pub async fn force_reannounce(&self) -> crate::Result<()> {
1352 self.cmd_tx
1353 .send(TorrentCommand::ForceReannounce)
1354 .await
1355 .map_err(|_| crate::Error::Shutdown)
1356 }
1357
1358 pub async fn scrape(&self) -> crate::Result<Option<(String, irontide_tracker::ScrapeInfo)>> {
1364 let (tx, rx) = oneshot::channel();
1365 self.cmd_tx
1366 .send(TorrentCommand::Scrape { reply: tx })
1367 .await
1368 .map_err(|_| crate::Error::Shutdown)?;
1369 rx.await.map_err(|_| crate::Error::Shutdown)
1370 }
1371
1372 pub async fn open_file(
1378 &self,
1379 file_index: usize,
1380 ) -> crate::Result<crate::streaming::FileStream> {
1381 let (tx, rx) = oneshot::channel();
1382 self.cmd_tx
1383 .send(TorrentCommand::OpenFile {
1384 file_index,
1385 reply: tx,
1386 })
1387 .await
1388 .map_err(|_| crate::Error::Shutdown)?;
1389 let handle = rx.await.map_err(|_| crate::Error::Shutdown)??;
1390 Ok(crate::streaming::FileStream::from_handle(handle))
1391 }
1392
1393 pub(crate) async fn update_external_ip(&self, ip: std::net::IpAddr) -> crate::Result<()> {
1395 self.cmd_tx
1396 .send(TorrentCommand::UpdateExternalIp { ip })
1397 .await
1398 .map_err(|_| crate::Error::Shutdown)
1399 }
1400
1401 pub async fn move_storage(&self, new_path: std::path::PathBuf) -> crate::Result<()> {
1410 let (tx, rx) = oneshot::channel();
1411 self.cmd_tx
1412 .send(TorrentCommand::MoveStorage {
1413 new_path,
1414 reply: tx,
1415 })
1416 .await
1417 .map_err(|_| crate::Error::Shutdown)?;
1418 rx.await.map_err(|_| crate::Error::Shutdown)?
1419 }
1420
1421 pub async fn set_download_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1427 let (tx, rx) = oneshot::channel();
1428 self.cmd_tx
1429 .send(TorrentCommand::SetDownloadLimit {
1430 bytes_per_sec,
1431 reply: tx,
1432 })
1433 .await
1434 .map_err(|_| crate::Error::Shutdown)?;
1435 rx.await.map_err(|_| crate::Error::Shutdown)
1436 }
1437
1438 pub async fn set_upload_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1444 let (tx, rx) = oneshot::channel();
1445 self.cmd_tx
1446 .send(TorrentCommand::SetUploadLimit {
1447 bytes_per_sec,
1448 reply: tx,
1449 })
1450 .await
1451 .map_err(|_| crate::Error::Shutdown)?;
1452 rx.await.map_err(|_| crate::Error::Shutdown)
1453 }
1454
1455 pub async fn download_limit(&self) -> crate::Result<u64> {
1461 let (tx, rx) = oneshot::channel();
1462 self.cmd_tx
1463 .send(TorrentCommand::DownloadLimit { reply: tx })
1464 .await
1465 .map_err(|_| crate::Error::Shutdown)?;
1466 rx.await.map_err(|_| crate::Error::Shutdown)
1467 }
1468
1469 pub async fn upload_limit(&self) -> crate::Result<u64> {
1475 let (tx, rx) = oneshot::channel();
1476 self.cmd_tx
1477 .send(TorrentCommand::UploadLimit { reply: tx })
1478 .await
1479 .map_err(|_| crate::Error::Shutdown)?;
1480 rx.await.map_err(|_| crate::Error::Shutdown)
1481 }
1482
1483 pub async fn set_sequential_download(&self, enabled: bool) -> crate::Result<()> {
1489 let (tx, rx) = oneshot::channel();
1490 self.cmd_tx
1491 .send(TorrentCommand::SetSequentialDownload { enabled, reply: tx })
1492 .await
1493 .map_err(|_| crate::Error::Shutdown)?;
1494 rx.await.map_err(|_| crate::Error::Shutdown)
1495 }
1496
1497 pub async fn is_sequential_download(&self) -> crate::Result<bool> {
1503 let (tx, rx) = oneshot::channel();
1504 self.cmd_tx
1505 .send(TorrentCommand::IsSequentialDownload { reply: tx })
1506 .await
1507 .map_err(|_| crate::Error::Shutdown)?;
1508 rx.await.map_err(|_| crate::Error::Shutdown)
1509 }
1510
1511 pub async fn set_super_seeding(&self, enabled: bool) -> crate::Result<()> {
1517 let (tx, rx) = oneshot::channel();
1518 self.cmd_tx
1519 .send(TorrentCommand::SetSuperSeeding { enabled, reply: tx })
1520 .await
1521 .map_err(|_| crate::Error::Shutdown)?;
1522 rx.await.map_err(|_| crate::Error::Shutdown)
1523 }
1524
1525 pub async fn is_super_seeding(&self) -> crate::Result<bool> {
1531 let (tx, rx) = oneshot::channel();
1532 self.cmd_tx
1533 .send(TorrentCommand::IsSuperSeeding { reply: tx })
1534 .await
1535 .map_err(|_| crate::Error::Shutdown)?;
1536 rx.await.map_err(|_| crate::Error::Shutdown)
1537 }
1538
1539 pub async fn set_seed_mode(&self, enabled: bool) -> crate::Result<()> {
1550 let (tx, rx) = oneshot::channel();
1551 self.cmd_tx
1552 .send(TorrentCommand::SetSeedMode { enabled, reply: tx })
1553 .await
1554 .map_err(|_| crate::Error::Shutdown)?;
1555 rx.await.map_err(|_| crate::Error::Shutdown)
1556 }
1557
1558 pub async fn add_tracker(&self, url: String) -> crate::Result<()> {
1566 self.cmd_tx
1567 .send(TorrentCommand::AddTracker { url })
1568 .await
1569 .map_err(|_| crate::Error::Shutdown)
1570 }
1571
1572 pub async fn replace_trackers(&self, urls: Vec<String>) -> crate::Result<()> {
1578 let (tx, rx) = oneshot::channel();
1579 self.cmd_tx
1580 .send(TorrentCommand::ReplaceTrackers { urls, reply: tx })
1581 .await
1582 .map_err(|_| crate::Error::Shutdown)?;
1583 rx.await.map_err(|_| crate::Error::Shutdown)
1584 }
1585
1586 pub async fn force_recheck(&self) -> crate::Result<()> {
1597 let (tx, rx) = oneshot::channel();
1598 self.cmd_tx
1599 .send(TorrentCommand::ForceRecheck { reply: tx })
1600 .await
1601 .map_err(|_| crate::Error::Shutdown)?;
1602 rx.await.map_err(|_| crate::Error::Shutdown)?
1603 }
1604
1605 pub async fn rename_file(&self, file_index: usize, new_name: String) -> crate::Result<()> {
1615 let (tx, rx) = oneshot::channel();
1616 self.cmd_tx
1617 .send(TorrentCommand::RenameFile {
1618 file_index,
1619 new_name,
1620 reply: tx,
1621 })
1622 .await
1623 .map_err(|_| crate::Error::Shutdown)?;
1624 rx.await.map_err(|_| crate::Error::Shutdown)?
1625 }
1626
1627 pub(crate) async fn spawn_ssl_peer(
1629 &self,
1630 addr: SocketAddr,
1631 stream: impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
1632 ) -> crate::Result<()> {
1633 self.cmd_tx
1634 .send(TorrentCommand::SpawnSslPeer {
1635 addr,
1636 stream: crate::types::BoxedAsyncStream(Box::new(stream)),
1637 })
1638 .await
1639 .map_err(|_| crate::Error::Shutdown)
1640 }
1641
1642 pub async fn set_max_connections(&self, limit: usize) -> crate::Result<()> {
1648 let (tx, rx) = oneshot::channel();
1649 self.cmd_tx
1650 .send(TorrentCommand::SetMaxConnections { limit, reply: tx })
1651 .await
1652 .map_err(|_| crate::Error::Shutdown)?;
1653 rx.await.map_err(|_| crate::Error::Shutdown)
1654 }
1655
1656 pub async fn max_connections(&self) -> crate::Result<usize> {
1662 let (tx, rx) = oneshot::channel();
1663 self.cmd_tx
1664 .send(TorrentCommand::MaxConnections { reply: tx })
1665 .await
1666 .map_err(|_| crate::Error::Shutdown)?;
1667 rx.await.map_err(|_| crate::Error::Shutdown)
1668 }
1669
1670 pub async fn set_max_uploads(&self, limit: usize) -> crate::Result<()> {
1676 let (tx, rx) = oneshot::channel();
1677 self.cmd_tx
1678 .send(TorrentCommand::SetMaxUploads { limit, reply: tx })
1679 .await
1680 .map_err(|_| crate::Error::Shutdown)?;
1681 rx.await.map_err(|_| crate::Error::Shutdown)
1682 }
1683
1684 pub async fn max_uploads(&self) -> crate::Result<usize> {
1690 let (tx, rx) = oneshot::channel();
1691 self.cmd_tx
1692 .send(TorrentCommand::MaxUploads { reply: tx })
1693 .await
1694 .map_err(|_| crate::Error::Shutdown)?;
1695 rx.await.map_err(|_| crate::Error::Shutdown)
1696 }
1697
1698 pub async fn get_peer_info(&self) -> crate::Result<Vec<PeerInfo>> {
1704 let (tx, rx) = oneshot::channel();
1705 self.cmd_tx
1706 .send(TorrentCommand::GetPeerInfo { reply: tx })
1707 .await
1708 .map_err(|_| crate::Error::Shutdown)?;
1709 rx.await.map_err(|_| crate::Error::Shutdown)
1710 }
1711
1712 pub async fn get_download_queue(&self) -> crate::Result<Vec<PartialPieceInfo>> {
1718 let (tx, rx) = oneshot::channel();
1719 self.cmd_tx
1720 .send(TorrentCommand::GetDownloadQueue { reply: tx })
1721 .await
1722 .map_err(|_| crate::Error::Shutdown)?;
1723 rx.await.map_err(|_| crate::Error::Shutdown)
1724 }
1725
1726 pub async fn have_piece(&self, index: u32) -> crate::Result<bool> {
1732 let (tx, rx) = oneshot::channel();
1733 self.cmd_tx
1734 .send(TorrentCommand::HavePiece { index, reply: tx })
1735 .await
1736 .map_err(|_| crate::Error::Shutdown)?;
1737 rx.await.map_err(|_| crate::Error::Shutdown)
1738 }
1739
1740 pub async fn piece_availability(&self) -> crate::Result<Vec<u32>> {
1746 let (tx, rx) = oneshot::channel();
1747 self.cmd_tx
1748 .send(TorrentCommand::PieceAvailability { reply: tx })
1749 .await
1750 .map_err(|_| crate::Error::Shutdown)?;
1751 rx.await.map_err(|_| crate::Error::Shutdown)
1752 }
1753
1754 pub async fn file_progress(&self) -> crate::Result<Vec<u64>> {
1760 let (tx, rx) = oneshot::channel();
1761 self.cmd_tx
1762 .send(TorrentCommand::FileProgress { reply: tx })
1763 .await
1764 .map_err(|_| crate::Error::Shutdown)?;
1765 rx.await.map_err(|_| crate::Error::Shutdown)
1766 }
1767
1768 pub async fn info_hashes(&self) -> crate::Result<irontide_core::InfoHashes> {
1774 let (tx, rx) = oneshot::channel();
1775 self.cmd_tx
1776 .send(TorrentCommand::InfoHashes { reply: tx })
1777 .await
1778 .map_err(|_| crate::Error::Shutdown)?;
1779 rx.await.map_err(|_| crate::Error::Shutdown)
1780 }
1781
1782 pub async fn torrent_file(&self) -> crate::Result<Option<TorrentMetaV1>> {
1790 let (tx, rx) = oneshot::channel();
1791 self.cmd_tx
1792 .send(TorrentCommand::TorrentFile { reply: tx })
1793 .await
1794 .map_err(|_| crate::Error::Shutdown)?;
1795 rx.await.map_err(|_| crate::Error::Shutdown)
1796 }
1797
1798 pub async fn torrent_file_v2(&self) -> crate::Result<Option<irontide_core::TorrentMetaV2>> {
1807 let (tx, rx) = oneshot::channel();
1808 self.cmd_tx
1809 .send(TorrentCommand::TorrentFileV2 { reply: tx })
1810 .await
1811 .map_err(|_| crate::Error::Shutdown)?;
1812 rx.await.map_err(|_| crate::Error::Shutdown)
1813 }
1814
1815 pub async fn force_dht_announce(&self) -> crate::Result<()> {
1823 self.cmd_tx
1824 .send(TorrentCommand::ForceDhtAnnounce)
1825 .await
1826 .map_err(|_| crate::Error::Shutdown)
1827 }
1828
1829 pub async fn read_piece(&self, index: u32) -> crate::Result<Bytes> {
1838 let (tx, rx) = oneshot::channel();
1839 self.cmd_tx
1840 .send(TorrentCommand::ReadPiece { index, reply: tx })
1841 .await
1842 .map_err(|_| crate::Error::Shutdown)?;
1843 rx.await.map_err(|_| crate::Error::Shutdown)?
1844 }
1845
1846 pub async fn flush_cache(&self) -> crate::Result<()> {
1852 let (tx, rx) = oneshot::channel();
1853 self.cmd_tx
1854 .send(TorrentCommand::FlushCache { reply: tx })
1855 .await
1856 .map_err(|_| crate::Error::Shutdown)?;
1857 rx.await.map_err(|_| crate::Error::Shutdown)?
1858 }
1859
1860 #[must_use]
1865 pub fn is_valid(&self) -> bool {
1866 !self.cmd_tx.is_closed()
1867 }
1868
1869 pub async fn clear_error(&self) -> crate::Result<()> {
1875 self.cmd_tx
1876 .send(TorrentCommand::ClearError)
1877 .await
1878 .map_err(|_| crate::Error::Shutdown)
1879 }
1880
1881 pub async fn file_status(&self) -> crate::Result<Vec<crate::types::FileStatus>> {
1889 let (tx, rx) = oneshot::channel();
1890 self.cmd_tx
1891 .send(TorrentCommand::FileStatus { reply: tx })
1892 .await
1893 .map_err(|_| crate::Error::Shutdown)?;
1894 rx.await.map_err(|_| crate::Error::Shutdown)
1895 }
1896
1897 pub async fn flags(&self) -> crate::Result<crate::types::TorrentFlags> {
1903 let (tx, rx) = oneshot::channel();
1904 self.cmd_tx
1905 .send(TorrentCommand::Flags { reply: tx })
1906 .await
1907 .map_err(|_| crate::Error::Shutdown)?;
1908 rx.await.map_err(|_| crate::Error::Shutdown)
1909 }
1910
1911 pub async fn set_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
1919 let (tx, rx) = oneshot::channel();
1920 self.cmd_tx
1921 .send(TorrentCommand::SetFlags { flags, reply: tx })
1922 .await
1923 .map_err(|_| crate::Error::Shutdown)?;
1924 rx.await.map_err(|_| crate::Error::Shutdown)
1925 }
1926
1927 pub async fn unset_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
1935 let (tx, rx) = oneshot::channel();
1936 self.cmd_tx
1937 .send(TorrentCommand::UnsetFlags { flags, reply: tx })
1938 .await
1939 .map_err(|_| crate::Error::Shutdown)?;
1940 rx.await.map_err(|_| crate::Error::Shutdown)
1941 }
1942
1943 pub async fn connect_peer(&self, addr: SocketAddr) -> crate::Result<()> {
1952 self.cmd_tx
1953 .send(TorrentCommand::ConnectPeer { addr })
1954 .await
1955 .map_err(|_| crate::Error::Shutdown)
1956 }
1957
1958 pub(crate) fn send_pre_resolved_metadata(&self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
1964 let _ = self
1965 .cmd_tx
1966 .try_send(TorrentCommand::PreResolvedMetadata { info_bytes, peers });
1967 }
1968
1969 #[cfg(feature = "test-util")]
1981 pub(crate) async fn test_inject_metadata(&self, info_bytes: Vec<u8>) -> crate::Result<()> {
1982 let (tx, rx) = tokio::sync::oneshot::channel();
1983 self.cmd_tx
1984 .send(TorrentCommand::TestInjectMetadata {
1985 info_bytes,
1986 reply: tx,
1987 })
1988 .await
1989 .map_err(|_| crate::Error::Shutdown)?;
1990 rx.await.map_err(|_| crate::Error::Shutdown)?;
1991 Ok(())
1992 }
1993}
1994
1995#[derive(Debug, Clone)]
2001pub(crate) struct CachedFileEntry {
2002 pub(crate) index: usize,
2003 #[allow(dead_code)] pub(crate) length: u64,
2005 pub(crate) first_piece: u32,
2006 pub(crate) last_piece: u32,
2007}
2008
2009#[derive(Debug, Clone)]
2011pub(crate) struct CachedFileInfo {
2012 pub(crate) entries: Vec<CachedFileEntry>,
2013}
2014
2015pub(crate) fn build_cached_file_info(meta: &TorrentMetaV1, lengths: &Lengths) -> CachedFileInfo {
2016 let piece_length = lengths.piece_length();
2017 let files = meta.info.files();
2018 let mut entries = Vec::with_capacity(files.len());
2019 let mut offset = 0u64;
2020 for (index, file) in files.iter().enumerate() {
2021 let first_piece = (offset / piece_length) as u32;
2022 let last_piece = if file.length == 0 {
2023 first_piece
2024 } else {
2025 ((offset + file.length - 1) / piece_length) as u32
2026 };
2027 entries.push(CachedFileEntry {
2028 index,
2029 length: file.length,
2030 first_piece,
2031 last_piece,
2032 });
2033 offset += file.length;
2034 }
2035 CachedFileInfo { entries }
2036}
2037
2038pub(crate) struct TorrentActor {
2043 pub(crate) config: TorrentConfig,
2044 pub(crate) lock_timing: crate::timed_lock::LockTimingSettings,
2046 pub(crate) info_hash: Id20,
2047 pub(crate) our_peer_id: Id20,
2048 pub(crate) state: TorrentState,
2049
2050 pub(crate) disk: Option<DiskHandle>,
2052 pub(crate) disk_manager: DiskManagerHandle,
2053 pub(crate) chunk_tracker: Option<ChunkTracker>,
2054 pub(crate) lengths: Option<Lengths>,
2055 pub(crate) num_pieces: u32,
2056
2057 pub(crate) file_priorities: Vec<FilePriority>,
2059 pub(crate) wanted_pieces: Bitfield,
2060 pub(crate) end_game: EndGame,
2061
2062 pub(crate) streaming_pieces: BTreeSet<u32>,
2064 pub(crate) time_critical_pieces: BTreeSet<u32>,
2065 pub(crate) streaming_cursors: Vec<crate::streaming::StreamingCursor>,
2066 pub(crate) piece_ready_tx: broadcast::Sender<u32>,
2067 pub(crate) have_watch_tx: tokio::sync::watch::Sender<Bitfield>,
2068 pub(crate) have_watch_rx: tokio::sync::watch::Receiver<Bitfield>,
2069 pub(crate) stream_read_semaphore: Arc<tokio::sync::Semaphore>,
2070
2071 pub(crate) peers: HashMap<SocketAddr, PeerState>,
2073 pub(crate) unchoke_durations: HashMap<SocketAddr, Duration>,
2082 pub(crate) cached_peer_rates: FxHashMap<SocketAddr, f64>,
2085 #[allow(dead_code)]
2087 pub(crate) refill_notify: Arc<tokio::sync::Notify>,
2088 pub(crate) atomic_states: Option<Arc<crate::piece_reservation::AtomicPieceStates>>,
2090 pub(crate) block_maps: Option<Arc<BlockMaps>>,
2092 pub(crate) steal_candidates: Option<Arc<StealCandidates>>,
2094 pub(crate) last_steal_populate: Instant,
2096 pub(crate) piece_write_guards: Option<Arc<crate::piece_reservation::PieceWriteGuards>>,
2098 pub(crate) soft_reap_buf: Vec<std::net::SocketAddr>,
2102 pub(crate) eviction_history: std::collections::VecDeque<std::time::Instant>,
2107 pub(crate) force_immediate_choker_tick: bool,
2112 pub(crate) piece_tracker: Option<PieceTracker>,
2114 pub(crate) order_map_tx: tokio::sync::watch::Sender<Arc<PieceOrderMap>>,
2116 pub(crate) piece_owner: Vec<Option<u16>>,
2118 pub(crate) peer_slab: crate::piece_reservation::PeerSlab,
2120 #[allow(dead_code)]
2121 pub(crate) priority_pieces: BTreeSet<u32>,
2122 pub(crate) max_in_flight: usize,
2124 pub(crate) reservation_notify: Option<Arc<tokio::sync::Notify>>,
2126 pub(crate) last_tick_dispatch_state: Option<(u32, usize)>,
2134 pub(crate) choker: Choker,
2135 pub(crate) user_seed_mode: bool,
2142 pub(crate) user_forced: bool,
2144 pub(crate) max_connections: usize,
2146 pub(crate) peer_states: Option<Arc<crate::peer_states::PeerStates>>,
2148 pub(crate) connect_semaphore: Arc<tokio::sync::Semaphore>,
2151 pub(crate) connect_permits:
2154 HashMap<SocketAddr, Arc<parking_lot::Mutex<Option<tokio::sync::OwnedSemaphorePermit>>>>,
2155 pub(crate) connect_rx: Option<mpsc::Receiver<ConnectPeer>>,
2157
2158 pub(crate) metadata_downloader: Option<MetadataDownloader>,
2160
2161 pub(crate) meta: Option<TorrentMetaV1>,
2163
2164 pub(crate) cached_files: Option<CachedFileInfo>,
2166
2167 pub(crate) downloaded: u64,
2169 pub(crate) uploaded: u64,
2170 pub(crate) checking_progress: f32,
2171 pub(crate) total_download: u64,
2172 pub(crate) total_upload: u64,
2173 pub(crate) total_failed_bytes: u64,
2174 pub(crate) total_redundant_bytes: u64,
2175 pub(crate) added_time: i64,
2176 pub(crate) completed_time: i64,
2177 pub(crate) last_download: i64,
2178 pub(crate) last_upload: i64,
2179 pub(crate) last_seen_complete: i64,
2180 pub(crate) active_duration: i64,
2181 pub(crate) finished_duration: i64,
2182 pub(crate) seeding_duration: i64,
2183 pub(crate) active_since: Option<std::time::Instant>,
2184 pub(crate) state_duration_since: Option<std::time::Instant>,
2185 #[allow(dead_code)] pub(crate) started_at: std::time::Instant,
2187 pub(crate) moving_storage: bool,
2188 pub(crate) has_incoming: bool,
2189 pub(crate) need_save_resume: bool,
2190 pub(crate) error: String,
2191 pub(crate) error_file: i32,
2192
2193 pub(crate) cmd_rx: mpsc::Receiver<TorrentCommand>,
2195 pub(crate) event_tx: mpsc::Sender<PeerEvent>,
2196 pub(crate) event_rx: mpsc::Receiver<PeerEvent>,
2197
2198 pub(crate) write_error_rx: mpsc::Receiver<crate::disk::DiskWriteError>,
2200 pub(crate) write_error_tx: mpsc::Sender<crate::disk::DiskWriteError>,
2201 pub(crate) verify_result_rx: mpsc::Receiver<crate::disk::VerifyResult>,
2202 pub(crate) verify_result_tx: mpsc::Sender<crate::disk::VerifyResult>,
2203 pub(crate) pending_verify: HashSet<u32>,
2206 pub(crate) piece_generations: Vec<u64>,
2209 pub(crate) hash_result_rx: tokio::sync::mpsc::Receiver<crate::hash_pool::HashResult>,
2211 pub(crate) hash_result_tx: tokio::sync::mpsc::Sender<crate::hash_pool::HashResult>,
2213
2214 pub(crate) listener: Option<Box<dyn crate::transport::TransportListener>>,
2216
2217 pub(crate) utp_socket: Option<irontide_utp::UtpSocket>,
2219 pub(crate) utp_socket_v6: Option<irontide_utp::UtpSocket>,
2221
2222 pub(crate) tracker_manager: TrackerManager,
2224 pub(crate) tracker_result_rx: Option<mpsc::Receiver<crate::tracker_manager::TrackerPeerBatch>>,
2227
2228 pub(crate) dht_rx: irontide_dht::DhtReceiver,
2235 pub(crate) dht_v6_rx: irontide_dht::DhtReceiver,
2236 pub(crate) dht_enabled: bool,
2237 pub(crate) dht_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2238 pub(crate) dht_v6_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2239 pub(crate) dht_v6_empty_count: u32,
2242 pub(crate) dht_v6_last_retry: Option<std::time::Instant>,
2244
2245 pub(crate) alert_tx: broadcast::Sender<Alert>,
2247 pub(crate) alert_mask: Arc<AtomicU32>,
2248
2249 pub(crate) upload_bucket: crate::rate_limiter::TokenBucket,
2251 pub(crate) download_bucket: SharedBucket,
2252 pub(crate) global_upload_bucket: Option<SharedBucket>,
2253 #[allow(dead_code)] pub(crate) global_download_bucket: Option<SharedBucket>,
2255 pub(crate) slot_tuner: crate::slot_tuner::SlotTuner,
2256 pub(crate) upload_bytes_interval: u64,
2257
2258 pub(crate) peak_download_rate: u64,
2260
2261 pub(crate) web_seeds: HashMap<String, mpsc::Sender<crate::web_seed::WebSeedCommand>>,
2263 pub(crate) banned_web_seeds: HashSet<String>,
2264 pub(crate) web_seed_in_flight: HashMap<u32, String>,
2265 pub(crate) web_seed_stats: HashMap<String, irontide_core::WebSeedStats>,
2269 pub(crate) pex_peer_count: usize,
2275 pub(crate) lsd_peer_count: usize,
2279
2280 pub(crate) super_seed: Option<crate::super_seed::SuperSeedState>,
2282 pub(crate) have_broadcast_tx: tokio::sync::broadcast::Sender<u32>,
2284
2285 pub(crate) suggested_to_peers: HashMap<SocketAddr, HashSet<u32>>,
2287
2288 pub(crate) predictive_have_sent: HashSet<u32>,
2290
2291 pub(crate) ban_manager: crate::session::SharedBanManager,
2293 pub(crate) piece_contributors: HashMap<u32, HashSet<std::net::IpAddr>>,
2294 pub(crate) parole_pieces: HashMap<u32, crate::ban::ParoleState>,
2295
2296 pub(crate) ip_filter: crate::session::SharedIpFilter,
2298
2299 pub(crate) external_ip: Option<std::net::IpAddr>,
2301
2302 pub(crate) share_lru: std::collections::VecDeque<u32>,
2306 pub(crate) share_max_pieces: usize,
2308
2309 pub(crate) plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
2311
2312 pub(crate) hash_picker: Option<irontide_core::HashPicker>,
2314 pub(crate) version: irontide_core::TorrentVersion,
2315 #[allow(dead_code)] pub(crate) meta_v2: Option<irontide_core::TorrentMetaV2>,
2317
2318 pub(crate) info_hashes: irontide_core::InfoHashes,
2320
2321 pub(crate) dht_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2323 pub(crate) dht_v6_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2324
2325 pub(crate) magnet_selected_files: Option<Vec<irontide_core::FileSelection>>,
2328
2329 pub(crate) sam_session: Option<Arc<crate::i2p::SamSession>>,
2331
2332 pub(crate) i2p_accept_rx: Option<mpsc::Receiver<crate::i2p::SamStream>>,
2334
2335 pub(crate) i2p_peer_counter: u32,
2337
2338 pub(crate) i2p_destinations: HashMap<SocketAddr, crate::i2p::I2pDestination>,
2340
2341 pub(crate) ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
2343
2344 pub(crate) rate_limiter_set: crate::rate_limiter::RateLimiterSet,
2346 pub(crate) auto_sequential_active: bool,
2348 pub(crate) factory: Arc<crate::transport::NetworkFactory>,
2350 pub(crate) hash_pool_ref: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
2352 pub(crate) live_outgoing_peers:
2354 std::sync::Arc<parking_lot::RwLock<std::collections::HashMap<SocketAddr, u8>>>,
2355 pub(crate) connect_attempts: u64,
2357 pub(crate) connect_failures: u64,
2359 pub(crate) choke_rotations: u64,
2361 pub(crate) inflight_started: Vec<Option<Instant>>,
2364 pub(crate) completed_piece_times: std::collections::VecDeque<Duration>,
2366 pub(crate) piece_steals: u64,
2368 pub(crate) holepunch_relayed: u64,
2370 pub(crate) holepunch_relay_rate: HashMap<SocketAddr, (Instant, u32)>,
2372 pub(crate) holepunch_cooldowns: HashMap<SocketAddr, Instant>,
2374 pub(crate) holepunch_pending: Vec<SocketAddr>,
2376 pub(crate) counters: Arc<crate::stats::SessionCounters>,
2380}
2381
2382pub(crate) const END_GAME_DEPTH: usize = 128;
2389
2390impl TorrentActor {
2393 pub(crate) fn current_dht(&self) -> Option<irontide_dht::DhtHandle> {
2399 if self.dht_enabled {
2400 self.dht_rx.current()
2401 } else {
2402 None
2403 }
2404 }
2405
2406 pub(crate) fn current_dht_v6(&self) -> Option<irontide_dht::DhtHandle> {
2409 if self.dht_enabled {
2410 self.dht_v6_rx.current()
2411 } else {
2412 None
2413 }
2414 }
2415
2416 #[allow(dead_code)] pub(crate) async fn current_dht_or_wait(
2432 &mut self,
2433 hold: std::time::Duration,
2434 ) -> Option<irontide_dht::DhtHandle> {
2435 if !self.dht_enabled {
2436 return None;
2437 }
2438 if let Some(handle) = self.dht_rx.current() {
2439 return Some(handle);
2440 }
2441 match tokio::time::timeout(hold, self.dht_rx.changed()).await {
2443 Ok(Ok(())) => self.dht_rx.current(),
2444 Ok(Err(_)) | Err(_) => None,
2445 }
2446 }
2447
2448 async fn run(mut self) {
2450 self.verify_existing_pieces().await;
2452
2453 if let Some(ct) = &self.chunk_tracker {
2456 let atomic_states = Arc::new(AtomicPieceStates::new(
2457 self.num_pieces,
2458 ct.bitfield(),
2459 &self.wanted_pieces,
2460 ));
2461 self.atomic_states = Some(Arc::clone(&atomic_states));
2462 self.piece_owner = vec![None; self.num_pieces as usize];
2463 self.inflight_started = vec![None; self.num_pieces as usize];
2465 self.max_in_flight = self.config.max_in_flight_pieces;
2466
2467 if self.config.use_block_stealing {
2469 if let Some(ref lengths) = self.lengths {
2470 self.block_maps = Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
2471 }
2472 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
2473 }
2474 self.piece_write_guards = Some(Arc::new(
2476 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
2477 ));
2478
2479 self.piece_tracker = Some(PieceTracker::new(
2481 self.num_pieces,
2482 ct.bitfield(),
2483 &self.wanted_pieces,
2484 ));
2485 if let Some(ref cached) = self.cached_files {
2486 let file_piece_ranges: Vec<(u32, u32)> = cached
2487 .entries
2488 .iter()
2489 .map(|e| (e.first_piece, e.last_piece))
2490 .collect();
2491 let om = Arc::new(PieceOrderMap::build(
2492 &self.file_priorities,
2493 &file_piece_ranges,
2494 self.num_pieces,
2495 0,
2496 ));
2497 self.order_map_tx.send_replace(om);
2498 }
2499
2500 let notify = Arc::new(tokio::sync::Notify::new());
2501 self.reservation_notify = Some(notify);
2502 }
2503
2504 if self.state != TorrentState::Seeding {
2506 self.spawn_web_seeds();
2507 self.assign_pieces_to_web_seeds();
2508 }
2509
2510 let connect_semaphore = Arc::new(tokio::sync::Semaphore::new(
2513 self.effective_max_connections(),
2514 ));
2515 self.connect_semaphore = Arc::clone(&connect_semaphore);
2516 self.connect_permits.clear();
2517 let (queue_tx, queue_rx) = mpsc::unbounded_channel();
2521 let peer_states = Arc::new(crate::peer_states::PeerStates::new_with_config(
2522 queue_tx,
2523 self.config.eviction_ban_set_cap,
2524 std::time::Duration::from_secs(self.config.eviction_ban_duration_secs),
2525 ));
2526 self.peer_states = Some(Arc::clone(&peer_states));
2527 let (adder_connect_tx, adder_connect_rx) = mpsc::channel(64);
2528 self.connect_rx = Some(adder_connect_rx);
2529 tokio::spawn(peer_adder::peer_adder_task(
2531 queue_rx,
2532 Arc::clone(&connect_semaphore),
2533 Arc::clone(&peer_states),
2534 Arc::clone(&self.ban_manager),
2535 Arc::clone(&self.ip_filter),
2536 adder_connect_tx,
2537 ));
2538
2539 let mut unchoke_interval = tokio::time::interval(Duration::from_secs(10));
2540 let mut rate_interval = tokio::time::interval(Duration::from_secs(2));
2541 rate_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2542 let mut optimistic_interval = tokio::time::interval(Duration::from_secs(30));
2543 let mut refill_interval = tokio::time::interval(Duration::from_millis(100));
2544 let mut dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::ZERO));
2545 let mut suggest_interval = if self.config.suggest_mode {
2546 Some(tokio::time::interval(Duration::from_secs(30)))
2547 } else {
2548 None
2549 };
2550 let mut turnover_interval = tokio::time::interval(Duration::from_secs(1));
2552 let mut pipeline_tick_interval = tokio::time::interval(Duration::from_secs(1));
2553 pipeline_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2555 let mut end_game_tick_interval = tokio::time::interval(Duration::from_millis(200));
2556 end_game_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2557 let mut diag_interval = tokio::time::interval(Duration::from_secs(5));
2558 let mut conn_stats_interval = tokio::time::interval(Duration::from_secs(30));
2560 let mut metadata_timeout_interval = tokio::time::interval(Duration::from_secs(5));
2562 let mut soft_reap_interval = tokio::time::interval(Duration::from_secs(1));
2565 let mut eviction_interval = tokio::time::interval(Duration::from_secs(2));
2568 eviction_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2569
2570 unchoke_interval.tick().await;
2572 optimistic_interval.tick().await;
2573 refill_interval.tick().await;
2574 if let Some(ref mut si) = suggest_interval {
2576 si.tick().await; }
2578 turnover_interval.tick().await;
2579 pipeline_tick_interval.tick().await;
2580 end_game_tick_interval.tick().await;
2581 diag_interval.tick().await;
2582 conn_stats_interval.tick().await;
2583 metadata_timeout_interval.tick().await;
2584 soft_reap_interval.tick().await;
2585 eviction_interval.tick().await;
2586
2587 if self.state == TorrentState::Downloading && self.config.enable_dht {
2590 if let Some(dht) = self.current_dht()
2592 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
2593 {
2594 warn!("DHT v4 announce failed: {e}");
2595 }
2596 if let Some(dht6) = self.current_dht_v6()
2597 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
2598 {
2599 debug!("DHT v6 announce failed: {e}");
2600 }
2601 if self.info_hashes.is_hybrid()
2603 && let Some(v2) = self.info_hashes.v2
2604 {
2605 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
2606 if v2_as_v1 != self.info_hash {
2607 if let Some(dht) = self.current_dht()
2608 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
2609 {
2610 debug!("DHT v4 dual-swarm announce failed: {e}");
2611 }
2612 if let Some(dht6) = self.current_dht_v6()
2613 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
2614 {
2615 debug!("DHT v6 dual-swarm announce failed: {e}");
2616 }
2617 }
2618 }
2619 }
2620
2621 if self.config.enable_i2p
2624 && let Some(ref sam) = self.sam_session
2625 {
2626 let (tx, rx) = mpsc::channel(16);
2627 let sam = Arc::clone(sam);
2628 tokio::spawn(async move {
2629 loop {
2630 match sam.accept().await {
2631 Ok(stream) => {
2632 if tx.send(stream).await.is_err() {
2633 break; }
2635 }
2636 Err(e) => {
2637 warn!("I2P accept error: {e}");
2638 tokio::time::sleep(Duration::from_secs(5)).await;
2639 }
2640 }
2641 }
2642 });
2643 self.i2p_accept_rx = Some(rx);
2644 }
2645
2646 loop {
2647 tokio::select! {
2648 biased;
2649 event = self.event_rx.recv() => {
2654 if let Some(event) = event {
2655 Self::ping_event_drain(&self.peers, &event);
2663 self.handle_peer_event(event)
2664 .instrument(tracing::debug_span!("handle_peer_event"))
2665 .await;
2666 for _ in 0..512 {
2668 match self.event_rx.try_recv() {
2669 Ok(event) => {
2670 Self::ping_event_drain(&self.peers, &event);
2671 self.handle_peer_event(event).await;
2672 }
2673 Err(_) => break,
2674 }
2675 }
2676 }
2677 }
2678 Some(result) = self.verify_result_rx.recv() => {
2680 self.pending_verify.remove(&result.piece);
2681 let dominated = self.chunk_tracker.as_ref()
2683 .is_some_and(|ct| ct.bitfield().get(result.piece));
2684 if !dominated {
2685 if result.passed {
2686 self.on_piece_verified(result.piece).await;
2687 } else {
2688 self.on_piece_hash_failed(result.piece).await;
2689 }
2691 }
2692 }
2693 Some(result) = self.hash_result_rx.recv() => {
2695 self.handle_hash_result(result).await;
2696 }
2697 cmd = self.cmd_rx.recv() => {
2699 match cmd {
2700 Some(TorrentCommand::AddPeers { peers, source }) => {
2701 self.handle_add_peers(peers, source);
2702 }
2703 Some(TorrentCommand::Stats { reply }) => {
2704 let _ = reply.send(self.make_stats());
2705 }
2706 Some(TorrentCommand::Pause) => {
2707 self.handle_pause().await;
2708 }
2709 Some(TorrentCommand::Queue) => {
2710 self.handle_queue();
2711 }
2712 Some(TorrentCommand::Resume) => {
2713 self.handle_resume().await;
2714 }
2715 Some(TorrentCommand::ForceResume) => {
2716 self.user_forced = true;
2717 self.handle_resume().await;
2718 }
2719 Some(TorrentCommand::SetCategory { category, reply }) => {
2720 self.config.category = category;
2724 self.need_save_resume = true;
2725 let _ = reply.send(());
2726 }
2727 Some(TorrentCommand::SetTags { tags, reply }) => {
2728 self.config.tags = tags;
2735 self.need_save_resume = true;
2736 let _ = reply.send(());
2737 }
2738 Some(TorrentCommand::GetWebSeeds { reply }) => {
2739 let urls = match &self.meta {
2745 Some(meta) => {
2746 let mut v = Vec::with_capacity(
2747 meta.url_list.len() + meta.httpseeds.len(),
2748 );
2749 v.extend(meta.url_list.iter().cloned());
2750 v.extend(meta.httpseeds.iter().cloned());
2751 v
2752 }
2753 None => Vec::new(),
2754 };
2755 let _ = reply.send(urls);
2756 }
2757 Some(TorrentCommand::GetPieceStates { reply }) => {
2758 let states = match self.atomic_states.as_ref() {
2762 Some(atomic) => atomic.snapshot(),
2763 None => Vec::new(),
2764 };
2765 let _ = reply.send(states);
2766 }
2767 Some(TorrentCommand::GetPieceHashes { offset, limit, reply }) => {
2768 let all_hashes: Vec<String> = match self.version {
2776 irontide_core::TorrentVersion::V1Only
2777 | irontide_core::TorrentVersion::Hybrid => self
2778 .meta
2779 .as_ref()
2780 .map(|meta| {
2781 meta.info
2782 .pieces
2783 .chunks_exact(20)
2784 .map(hex::encode)
2785 .collect::<Vec<String>>()
2786 })
2787 .unwrap_or_default(),
2788 irontide_core::TorrentVersion::V2Only => self
2789 .meta_v2
2790 .as_ref()
2791 .map(|m| {
2792 m.piece_layers
2793 .values()
2794 .flat_map(|v| v.chunks_exact(32))
2795 .map(hex::encode)
2796 .collect::<Vec<String>>()
2797 })
2798 .unwrap_or_default(),
2799 };
2800
2801 let start = (offset as usize).min(all_hashes.len());
2802 let end = start
2803 .saturating_add(limit as usize)
2804 .min(all_hashes.len());
2805 let slice = all_hashes
2806 .get(start..end)
2807 .map(<[String]>::to_vec)
2808 .unwrap_or_default();
2809 let _ = reply.send(slice);
2810 }
2811 Some(TorrentCommand::SaveResumeData { reply }) => {
2812 let result = self.build_resume_data();
2813 let _ = reply.send(result);
2814 }
2815 Some(TorrentCommand::SetFilePriority { index, priority, reply }) => {
2816 let result = self.handle_set_file_priority(index, priority);
2817 if result.is_ok() {
2818 self.sync_piece_states_with_wanted();
2819 if let Some(ref cached) = self.cached_files {
2821 let file_piece_ranges: Vec<(u32, u32)> = cached
2822 .entries
2823 .iter()
2824 .map(|e| (e.first_piece, e.last_piece))
2825 .collect();
2826 let next_gen = self.order_map_tx.borrow().generation + 1;
2827 let om = Arc::new(PieceOrderMap::build(
2828 &self.file_priorities,
2829 &file_piece_ranges,
2830 self.num_pieces,
2831 next_gen,
2832 ));
2833 self.order_map_tx.send_replace(om);
2834 }
2835 if let Some(ref mut pt) = self.piece_tracker {
2836 for piece in 0..self.num_pieces {
2837 if self.wanted_pieces.get(piece) {
2838 pt.mark_wanted(piece);
2839 } else {
2840 pt.mark_unwanted(piece);
2841 }
2842 }
2843 }
2844 }
2845 let _ = reply.send(result);
2846 }
2847 Some(TorrentCommand::FilePriorities { reply }) => {
2848 let _ = reply.send(self.file_priorities.clone());
2849 }
2850 Some(TorrentCommand::ForceReannounce) => {
2851 self.tracker_manager.force_reannounce();
2852 }
2853 Some(TorrentCommand::TrackerList { reply }) => {
2854 let _ = reply.send(self.tracker_manager.tracker_list());
2855 }
2856 Some(TorrentCommand::Scrape { reply }) => {
2857 let result = self.tracker_manager.scrape().await;
2858 if let Some((ref url, ref info)) = result {
2859 post_alert(&self.alert_tx, &self.alert_mask, AlertKind::ScrapeReply {
2860 info_hash: self.info_hash,
2861 url: url.clone(),
2862 complete: info.complete,
2863 incomplete: info.incomplete,
2864 downloaded: info.downloaded,
2865 });
2866 }
2867 let _ = reply.send(result);
2868 }
2869 Some(TorrentCommand::OpenFile { file_index, reply }) => {
2870 let result = self.handle_open_file(file_index);
2871 let _ = reply.send(result);
2872 }
2873 Some(TorrentCommand::IncomingPeer { stream, addr }) => {
2874 self.spawn_peer_from_stream_with_mode(
2875 addr,
2876 stream,
2877 Some(irontide_wire::mse::EncryptionMode::Disabled),
2878 );
2879 }
2880 Some(TorrentCommand::UpdateExternalIp { ip }) => {
2881 self.external_ip = Some(ip);
2882 post_alert(
2883 &self.alert_tx,
2884 &self.alert_mask,
2885 AlertKind::ExternalIpDetected { ip },
2886 );
2887 }
2888 Some(TorrentCommand::MoveStorage { new_path, reply }) => {
2889 let result = self.handle_move_storage(new_path).await;
2890 let _ = reply.send(result);
2891 }
2892 Some(TorrentCommand::SpawnSslPeer { addr, stream }) => {
2893 self.spawn_peer_from_stream_with_mode(
2895 addr,
2896 stream.0,
2897 Some(irontide_wire::mse::EncryptionMode::Disabled),
2898 );
2899 }
2900 Some(TorrentCommand::SetDownloadLimit { bytes_per_sec, reply }) => {
2901 self.download_bucket.lock().set_rate(bytes_per_sec);
2902 let _ = reply.send(());
2903 }
2904 Some(TorrentCommand::SetUploadLimit { bytes_per_sec, reply }) => {
2905 self.upload_bucket.set_rate(bytes_per_sec);
2906 let _ = reply.send(());
2907 }
2908 Some(TorrentCommand::DownloadLimit { reply }) => {
2909 let _ = reply.send(self.download_bucket.lock().rate());
2910 }
2911 Some(TorrentCommand::UploadLimit { reply }) => {
2912 let _ = reply.send(self.upload_bucket.rate());
2913 }
2914 Some(TorrentCommand::SetSequentialDownload { enabled, reply }) => {
2915 self.config.sequential_download = enabled;
2916 let _ = reply.send(());
2917 }
2918 Some(TorrentCommand::IsSequentialDownload { reply }) => {
2919 let _ = reply.send(self.config.sequential_download);
2920 }
2921 Some(TorrentCommand::SetSuperSeeding { enabled, reply }) => {
2922 self.config.super_seeding = enabled;
2923 self.super_seed = if enabled {
2924 Some(crate::super_seed::SuperSeedState::new())
2925 } else {
2926 None
2927 };
2928 let _ = reply.send(());
2929 }
2930 Some(TorrentCommand::IsSuperSeeding { reply }) => {
2931 let _ = reply.send(self.config.super_seeding);
2932 }
2933 Some(TorrentCommand::SetSeedMode { enabled, reply }) => {
2934 self.handle_set_seed_mode(enabled);
2935 let _ = reply.send(());
2936 }
2937 Some(TorrentCommand::SetSeedRatioLimit { limit, reply }) => {
2938 self.config.seed_ratio_limit = limit;
2939 self.need_save_resume = true;
2940 let _ = reply.send(());
2941 }
2942 Some(TorrentCommand::AddTracker { url }) => {
2943 self.tracker_manager.add_tracker_url(&url);
2944 }
2945 Some(TorrentCommand::ReplaceTrackers { urls, reply }) => {
2946 self.tracker_manager.replace_all(&urls);
2947 let _ = reply.send(());
2948 }
2949 Some(TorrentCommand::ForceRecheck { reply }) => {
2950 self.handle_force_recheck(reply).await;
2951 }
2952 Some(TorrentCommand::RenameFile { file_index, new_name, reply }) => {
2953 let result = self.handle_rename_file(file_index, new_name).await;
2954 let _ = reply.send(result);
2955 }
2956 Some(TorrentCommand::SetMaxConnections { limit, reply }) => {
2957 self.max_connections = limit;
2958 let _ = reply.send(());
2959 }
2960 Some(TorrentCommand::MaxConnections { reply }) => {
2961 let _ = reply.send(self.max_connections);
2962 }
2963 Some(TorrentCommand::SetMaxUploads { limit, reply }) => {
2964 self.choker.set_unchoke_slots(limit);
2965 let _ = reply.send(());
2966 }
2967 Some(TorrentCommand::MaxUploads { reply }) => {
2968 let _ = reply.send(self.choker.unchoke_slots());
2969 }
2970 Some(TorrentCommand::GetPeerInfo { reply }) => {
2971 let _ = reply.send(self.build_peer_info());
2972 }
2973 Some(TorrentCommand::GetDownloadQueue { reply }) => {
2974 let _ = reply.send(self.build_download_queue());
2975 }
2976 Some(TorrentCommand::HavePiece { index, reply }) => {
2977 let has = self.chunk_tracker.as_ref()
2978 .is_some_and(|ct| ct.has_piece(index));
2979 let _ = reply.send(has);
2980 }
2981 Some(TorrentCommand::PieceAvailability { reply }) => {
2982 let mut avail = vec![0u32; self.num_pieces as usize];
2983 for peer in self.peers.values() {
2984 for i in 0..self.num_pieces {
2985 if peer.bitfield.get(i) {
2986 avail[i as usize] += 1;
2987 }
2988 }
2989 }
2990 let _ = reply.send(avail);
2991 }
2992 Some(TorrentCommand::FileProgress { reply }) => {
2993 let _ = reply.send(self.compute_file_progress());
2994 }
2995 Some(TorrentCommand::InfoHashes { reply }) => {
2996 let _ = reply.send(self.info_hashes.clone());
2997 }
2998 Some(TorrentCommand::TorrentFile { reply }) => {
2999 let _ = reply.send(self.meta.clone());
3000 }
3001 Some(TorrentCommand::TorrentFileV2 { reply }) => {
3002 let _ = reply.send(self.meta_v2.clone());
3003 }
3004 Some(TorrentCommand::ForceDhtAnnounce) => {
3005 self.handle_force_dht_announce().await;
3006 }
3007 Some(TorrentCommand::ReadPiece { index, reply }) => {
3008 let result = self.handle_read_piece(index).await;
3009 let _ = reply.send(result);
3010 }
3011 Some(TorrentCommand::FlushCache { reply }) => {
3012 let result = self.handle_flush_cache().await;
3013 let _ = reply.send(result);
3014 }
3015 Some(TorrentCommand::ClearError) => {
3016 self.handle_clear_error().await;
3017 }
3018 Some(TorrentCommand::ClearSaveResumeFlag) => {
3019 self.need_save_resume = false;
3020 }
3021 Some(TorrentCommand::RestoreResumeBitmap { pieces, reply }) => {
3022 let result = self.handle_restore_resume_bitmap(pieces);
3023 let _ = reply.send(result);
3024 }
3025 Some(TorrentCommand::RestoreWebSeedStats { stats, reply }) => {
3026 self.web_seed_stats = stats;
3027 let _ = reply.send(Ok(()));
3028 }
3029 Some(TorrentCommand::GetPeerSourceCounts { reply }) => {
3030 let _ = reply.send((self.pex_peer_count, self.lsd_peer_count));
3031 }
3032 Some(TorrentCommand::QueryUnchokeDurations { reply }) => {
3033 let mut out = self.unchoke_durations.clone();
3034 let now = Instant::now();
3037 for peer in self.peers.values() {
3038 let mut delta = peer.unchoke_duration_total;
3039 if let Some(start) = peer.am_unchoke_started_at {
3040 delta += now.duration_since(start);
3041 }
3042 if !delta.is_zero() {
3043 *out.entry(peer.addr).or_default() += delta;
3044 }
3045 }
3046 let _ = reply.send(out);
3047 }
3048 Some(TorrentCommand::GetWebSeedStats { reply }) => {
3049 let snapshot: Vec<_> = self.web_seed_stats.values().cloned().collect();
3050 let _ = reply.send(snapshot);
3051 }
3052 Some(TorrentCommand::FileStatus { reply }) => {
3053 let _ = reply.send(self.build_file_status());
3054 }
3055 Some(TorrentCommand::Flags { reply }) => {
3056 let _ = reply.send(self.build_flags());
3057 }
3058 Some(TorrentCommand::SetFlags { flags, reply }) => {
3059 self.apply_set_flags(flags).await;
3060 let _ = reply.send(());
3061 }
3062 Some(TorrentCommand::UnsetFlags { flags, reply }) => {
3063 self.apply_unset_flags(flags).await;
3064 let _ = reply.send(());
3065 }
3066 Some(TorrentCommand::ConnectPeer { addr }) => {
3067 self.handle_connect_peer(addr);
3068 }
3069 Some(TorrentCommand::PreResolvedMetadata { info_bytes, peers }) => {
3070 self.handle_pre_resolved_metadata(info_bytes, peers).await;
3071 }
3072 #[cfg(feature = "test-util")]
3073 Some(TorrentCommand::TestInjectMetadata { info_bytes, reply }) => {
3074 self.handle_pre_resolved_metadata(info_bytes, vec![]).await;
3078 let _ = reply.send(());
3079 }
3080 Some(TorrentCommand::GetMeta { reply }) => {
3081 let _ = reply.send(self.meta.clone());
3086 }
3087 Some(TorrentCommand::UpdateSettings(delta)) => {
3088 self.handle_update_settings(&delta);
3089 }
3090 Some(TorrentCommand::Shutdown) => {
3091 info!("torrent actor: received Shutdown command, exiting");
3092 self.shutdown_web_seeds().await;
3093 self.shutdown_peers().await;
3094 return;
3095 }
3096 None => {
3097 warn!("torrent actor: cmd_rx channel closed (all senders dropped), exiting");
3098 self.shutdown_web_seeds().await;
3099 self.shutdown_peers().await;
3100 return;
3101 }
3102 }
3103 }
3104 Some(err) = self.write_error_rx.recv() => {
3106 warn!(piece = err.piece, begin = err.begin, "async disk write failed: {}", err.error);
3107 }
3108 result = accept_incoming(&mut self.listener) => {
3110 if let Ok((stream, addr)) = result {
3111 self.spawn_peer_from_stream(addr, stream);
3112 }
3113 }
3114 stream = accept_i2p(&mut self.i2p_accept_rx) => {
3116 if let Some(stream) = stream {
3117 self.handle_i2p_incoming(stream);
3118 }
3119 }
3120 _ = rate_interval.tick() => {
3123 self.update_peer_rates();
3124 }
3125 _ = unchoke_interval.tick() => {
3127 if self.state == TorrentState::Seeding
3136 || self.state == TorrentState::Sharing
3137 {
3138 self.slot_tuner.observe(self.upload_bytes_interval);
3139 self.choker.observe_throughput(self.upload_bytes_interval);
3140 self.upload_bytes_interval = 0;
3141 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3142 self.run_choker().await;
3143 self.force_immediate_choker_tick = false;
3146 } else {
3147 self.upload_bytes_interval = 0;
3148 }
3149 self.update_streaming_cursors();
3151 if self.config.auto_sequential {
3153 self.auto_sequential_active = crate::piece_selector::evaluate_auto_sequential(
3154 self.piece_owner.iter().filter(|o| o.is_some()).count(),
3155 self.peers.len(),
3156 self.auto_sequential_active,
3157 );
3158 }
3159 self.assign_pieces_to_web_seeds();
3161 }
3162 _ = optimistic_interval.tick() => {
3164 self.rotate_optimistic();
3165 }
3166 Some(connect_peer) = async {
3168 match self.connect_rx.as_mut() {
3169 Some(rx) => rx.recv().await,
3170 None => std::future::pending().await,
3171 }
3172 } => {
3173 self.handle_adder_connect(connect_peer);
3174 }
3175 () = &mut dht_requery_sleep, if self.state != TorrentState::Complete
3176 && self.state != TorrentState::Paused
3177 && self.state != TorrentState::Queued
3178 && self.state != TorrentState::Seeding
3179 && self.state != TorrentState::Stopped => {
3180 self.run_dht_requery().await;
3181 dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::from_mins(1)));
3182 }
3183 () = async {
3188 match self.tracker_manager.next_announce_in() {
3189 Some(dur) => tokio::time::sleep(dur).await,
3190 None => std::future::pending().await,
3191 }
3192 }, if self.tracker_result_rx.is_none() => {
3193 let left = self.calculate_left();
3194 self.tracker_result_rx = Some(self.tracker_manager.start_announce(
3195 irontide_tracker::AnnounceEvent::None,
3196 self.uploaded,
3197 self.downloaded,
3198 left,
3199 ));
3200 }
3201 result = async {
3204 match self.tracker_result_rx.as_mut() {
3205 Some(rx) => rx.recv().await,
3206 None => std::future::pending().await,
3207 }
3208 } => {
3209 match result {
3210 Some(batch) => {
3211 let (peers, outcome) = self.tracker_manager.process_tracker_result(batch);
3212 self.fire_tracker_alerts(&[outcome]);
3213 if !peers.is_empty() {
3214 debug!(count = peers.len(), "tracker returned peers (streaming)");
3215 self.handle_add_peers(peers, PeerSource::Tracker);
3216 }
3217 }
3218 None => {
3219 self.tracker_result_rx = None;
3222 }
3223 }
3224 }
3225 result = async {
3227 match &mut self.dht_peers_rx {
3228 Some(rx) => rx.recv().await,
3229 None => std::future::pending().await,
3230 }
3231 } => {
3232 if let Some(peers) = result {
3233 debug!(count = peers.len(), "DHT v4 returned peers");
3234 self.handle_add_peers(peers, PeerSource::Dht);
3235 } else {
3236 debug!("DHT v4 peer search exhausted");
3237 self.dht_peers_rx = None;
3238 }
3239 }
3240 result = async {
3242 match &mut self.dht_v6_peers_rx {
3243 Some(rx) => rx.recv().await,
3244 None => std::future::pending().await,
3245 }
3246 } => {
3247 if let Some(peers) = result {
3248 debug!(count = peers.len(), "DHT v6 returned peers");
3249 self.dht_v6_empty_count = 0; self.handle_add_peers(peers, PeerSource::Dht);
3251 } else {
3252 self.dht_v6_peers_rx = None;
3253 self.dht_v6_empty_count += 1;
3254 if self.dht_v6_empty_count == 30 {
3255 debug!("DHT v6 routing table persistently empty, giving up");
3256 } else if self.dht_v6_empty_count < 30 {
3257 debug!("DHT v6 peer search exhausted");
3258 }
3259 }
3260 }
3261 result = async {
3263 match &mut self.dht_v2_peers_rx {
3264 Some(rx) => rx.recv().await,
3265 None => std::future::pending().await,
3266 }
3267 } => {
3268 if let Some(peers) = result {
3269 debug!(count = peers.len(), "DHT v4 v2-swarm returned peers");
3270 self.handle_add_peers(peers, PeerSource::Dht);
3271 } else {
3272 debug!("DHT v4 v2-swarm peer search exhausted");
3273 self.dht_v2_peers_rx = None;
3274 }
3275 }
3276 result = async {
3278 match &mut self.dht_v6_v2_peers_rx {
3279 Some(rx) => rx.recv().await,
3280 None => std::future::pending().await,
3281 }
3282 } => {
3283 if let Some(peers) = result {
3284 debug!(count = peers.len(), "DHT v6 v2-swarm returned peers");
3285 self.handle_add_peers(peers, PeerSource::Dht);
3286 } else {
3287 debug!("DHT v6 v2-swarm peer search exhausted");
3288 self.dht_v6_v2_peers_rx = None;
3289 }
3290 }
3291 _ = async {
3293 match suggest_interval {
3294 Some(ref mut interval) => interval.tick().await,
3295 None => std::future::pending().await,
3296 }
3297 } => {
3298 self.suggest_cached_pieces().await;
3299 }
3300 _ = turnover_interval.tick() => {
3301 self.run_steal_queue_maintenance();
3302 }
3303 _ = pipeline_tick_interval.tick() => {
3305 let snub_timeout = Duration::from_secs(u64::from(self.config.snub_timeout_secs));
3306
3307 for (_addr, peer) in &mut self.peers {
3308 peer.pipeline.tick();
3309
3310 if !peer.peer_choking && !peer.snubbed {
3312 let idle = peer.last_data_received
3313 .is_some_and(|t| t.elapsed() > snub_timeout);
3314 if idle {
3315 peer.snubbed = true;
3316 peer.blocks_timed_out = peer.blocks_timed_out
3318 .saturating_add(peer.pending_requests.len() as u64);
3319 debug!(%_addr, "peer snubbed (no data for {}s)", self.config.snub_timeout_secs);
3320 }
3321 }
3322 }
3323
3324 self.refresh_peer_rates();
3327
3328 if !self.end_game.is_active() {
3330 self.check_end_game_activation();
3331 }
3332
3333 self.tick_dispatch_safety_wake();
3334
3335 if self.config.choke_rotation_max_evictions > 0
3337 && self.state == TorrentState::Downloading
3338 {
3339 self.run_choke_rotation();
3340 }
3341 }
3342 _ = end_game_tick_interval.tick(), if self.end_game.is_active() => {
3347 let addrs: Vec<SocketAddr> = self.peers.iter()
3348 .filter(|(_, p)| !p.peer_choking && p.pending_requests.len() < END_GAME_DEPTH)
3349 .map(|(addr, _)| *addr)
3350 .collect();
3351 for addr in addrs {
3352 self.request_end_game_block(addr).await;
3353 }
3354 }
3355 _ = metadata_timeout_interval.tick(), if self.state == TorrentState::FetchingMetadata => {
3358 let timed_out: Vec<u32> = self
3360 .metadata_downloader
3361 .as_ref()
3362 .map(MetadataDownloader::timed_out_pieces)
3363 .unwrap_or_default();
3364
3365 if !timed_out.is_empty() {
3366 debug!(count = timed_out.len(), "metadata pieces timed out, re-requesting");
3367
3368 let eligible_senders: Vec<mpsc::Sender<PeerCommand>> = self
3371 .peers
3372 .iter()
3373 .filter(|(addr, peer)| {
3374 self.metadata_downloader
3375 .as_ref()
3376 .is_some_and(|dl| !dl.is_rejected(addr))
3377 && peer
3378 .ext_handshake
3379 .as_ref()
3380 .is_some_and(|h| h.metadata_size.is_some())
3381 })
3382 .map(|(_, peer)| peer.cmd_tx.clone())
3383 .collect();
3384
3385 for cmd_tx in &eligible_senders {
3387 for &piece in &timed_out {
3388 let _ = cmd_tx.try_send(PeerCommand::RequestMetadata { piece });
3389 }
3390 }
3391
3392 if let Some(ref mut dl) = self.metadata_downloader {
3394 for piece in timed_out {
3395 dl.reset_request_time(piece);
3396 }
3397 }
3398 }
3399 }
3400 _ = diag_interval.tick() => {
3402 {
3404 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3405 let eg = self.end_game.is_active();
3406 let eg_blocks = self.end_game.block_count();
3407 info!(state = ?self.state, have, total = self.num_pieces, end_game = eg, eg_blocks, "heartbeat");
3408 }
3409 if self.state == TorrentState::Downloading {
3410 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3411 let in_flight = self.atomic_states.as_ref().map_or(0, |s| s.in_flight_count() as usize);
3412 let unchoked = self.peers.values().filter(|p| !p.peer_choking).count();
3413 info!(have, in_flight, total = self.num_pieces,
3414 downloaded_mb = self.downloaded / (1024 * 1024),
3415 peers = self.peers.len(), unchoked,
3416 "download progress");
3417 for (addr, p) in &self.peers {
3418 let last_data = p.last_data_received.map_or(9999, |t| t.elapsed().as_secs());
3419 trace!(%addr,
3420 choking = p.peer_choking,
3421 pending = p.pending_requests.len(),
3422 ewma_rate = p.pipeline.ewma_rate() as u64,
3423 last_data_secs = last_data,
3424 bf_ones = p.bitfield.count_ones(),
3425 "peer state");
3426 }
3427 }
3428 }
3429 _ = conn_stats_interval.tick() => {
3431 if self.connect_attempts > 0 {
3432 let succeeded = self.connect_attempts.saturating_sub(self.connect_failures);
3433 let success_pct = (succeeded as f64 / self.connect_attempts as f64 * 100.0) as u32;
3434 info!(
3435 connected = self.peers.len(),
3436 attempted = self.connect_attempts,
3437 failed = self.connect_failures,
3438 success_rate = %format!("{success_pct}%"),
3439 "connection stats"
3440 );
3441 }
3442 }
3443 _ = soft_reap_interval.tick() => {
3449 let soft_timeout = self.config.connect_soft_timeout;
3450 if soft_timeout > 0 {
3451 if let Some(ref ps) = self.peer_states {
3452 ps.soft_reap_candidates_into(
3453 Duration::from_secs(soft_timeout),
3454 &mut self.soft_reap_buf,
3455 );
3456 } else {
3457 self.soft_reap_buf.clear();
3458 }
3459 for i in 0..self.soft_reap_buf.len() {
3460 let peer_addr = self.soft_reap_buf[i];
3461 debug!(%peer_addr, soft_timeout, "soft reap: no TCP SYN-ACK");
3462 self.connect_permits.remove(&peer_addr);
3464 self.disconnect_peer(peer_addr, "soft reap: no TCP SYN-ACK");
3465 if let Some(ref ps) = self.peer_states
3466 && let Some(backoff) = ps.mark_dead(peer_addr)
3467 {
3468 let ps_clone = Arc::clone(ps);
3469 tokio::spawn(async move {
3470 tokio::time::sleep(backoff).await;
3471 ps_clone.mark_queued_for_retry(peer_addr);
3472 });
3473 }
3474 }
3475 self.soft_reap_buf.clear();
3476 }
3477 }
3478 _ = eviction_interval.tick() => {
3501 if self.force_immediate_choker_tick
3507 && (self.state == TorrentState::Seeding
3508 || self.state == TorrentState::Sharing)
3509 {
3510 self.slot_tuner.observe(self.upload_bytes_interval);
3511 self.choker.observe_throughput(self.upload_bytes_interval);
3512 self.upload_bytes_interval = 0;
3513 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3514 self.run_choker().await;
3515 self.force_immediate_choker_tick = false;
3516 }
3517 if self.state != TorrentState::Seeding {
3518 let prune_cutoff = std::time::Duration::from_mins(1);
3521 while self
3522 .eviction_history
3523 .front()
3524 .copied()
3525 .is_some_and(|t| t.elapsed() > prune_cutoff)
3526 {
3527 self.eviction_history.pop_front();
3528 }
3529 let limit = self.config.proactive_evictions_per_minute_limit as usize;
3530 let window_ok = self.eviction_history.len() < limit;
3531
3532 let should_evict = window_ok
3536 && self.peer_states.as_ref().is_some_and(|ps| {
3537 let live = ps
3538 .stats
3539 .live
3540 .load(std::sync::atomic::Ordering::Relaxed);
3541 #[allow(
3542 clippy::cast_possible_truncation,
3543 clippy::cast_sign_loss
3544 )]
3545 let threshold =
3546 (self.effective_max_connections() as f32 * 0.95) as u32;
3547 debug_assert!(
3548 self.effective_max_connections()
3549 <= crate::torrent_peers::HARD_PEER_CEILING,
3550 "effective_max must be clamped to HARD_PEER_CEILING"
3551 );
3552 live >= threshold
3553 });
3554 if should_evict {
3555 let max_this_tick = 5.min(limit.saturating_sub(self.eviction_history.len()));
3558 for _ in 0..max_this_tick {
3559 match self.find_eviction_candidate() {
3560 Some((victim, pass)) => {
3561 debug!(%victim, ?pass, "v0.187.3 proactive eviction");
3562 self.disconnect_peer(victim, "proactive eviction");
3563 if matches!(pass, crate::torrent_peers::EvictionPass::ZeroThroughput)
3564 && let Some(ref ps) = self.peer_states
3565 {
3566 ps.add_eviction_ban(victim);
3567 }
3568 self.eviction_history.push_back(std::time::Instant::now());
3569 }
3570 None => break,
3571 }
3572 }
3573 }
3574
3575 self.run_piece_steal_scan();
3577 }
3578 }
3579 _ = refill_interval.tick() => {
3581 let elapsed = Duration::from_millis(100);
3582 self.upload_bucket.refill(elapsed);
3583 self.download_bucket.lock().refill(elapsed);
3584 self.rate_limiter_set.refill(elapsed);
3586 let (tcp_peers, utp_peers) = self.transport_peer_counts();
3587 self.rate_limiter_set.apply_mixed_mode(
3588 self.config.mixed_mode_algorithm,
3589 tcp_peers,
3590 utp_peers,
3591 self.config.upload_rate_limit,
3592 );
3593 }
3594 }
3595
3596 for target in std::mem::take(&mut self.holepunch_pending) {
3598 self.try_holepunch(target).await;
3599 }
3600 }
3601 }
3602
3603 pub(crate) fn distributed_copies(&self) -> (u32, u32, f32) {
3609 if self.num_pieces == 0 || self.peers.is_empty() {
3610 return (0, 0, 0.0);
3611 }
3612
3613 let num = self.num_pieces as usize;
3614 let mut availability = vec![0u32; num];
3615
3616 for peer in self.peers.values() {
3617 for idx in 0..self.num_pieces {
3618 if peer.bitfield.get(idx) {
3619 availability[idx as usize] += 1;
3620 }
3621 }
3622 }
3623
3624 let min_avail = availability.iter().copied().min().unwrap_or(0);
3625 let rarest_count = availability.iter().filter(|&&c| c == min_avail).count() as u32;
3626 let fraction = ((self.num_pieces - rarest_count) * 1000) / self.num_pieces;
3627 let copies_float = min_avail as f32 + fraction as f32 / 1000.0;
3628
3629 (min_avail, fraction, copies_float)
3630 }
3631
3632 fn build_download_queue(&self) -> Vec<PartialPieceInfo> {
3633 self.piece_owner
3634 .iter()
3635 .enumerate()
3636 .filter_map(|(piece_index, owner)| {
3637 owner.map(|_| {
3638 let piece_index = piece_index as u32;
3639 let blocks_in_piece = self
3640 .lengths
3641 .as_ref()
3642 .map_or(0, |l| l.piece_size(piece_index).div_ceil(l.chunk_size()));
3643 PartialPieceInfo {
3644 piece_index,
3645 blocks_in_piece,
3646 blocks_assigned: 0,
3647 }
3648 })
3649 })
3650 .collect()
3651 }
3652
3653 fn compute_file_progress(&self) -> Vec<u64> {
3655 let Some(meta) = self.meta.as_ref() else {
3656 return Vec::new();
3657 };
3658 let Some(lengths) = self.lengths.as_ref() else {
3659 return Vec::new();
3660 };
3661 let Some(chunk_tracker) = self.chunk_tracker.as_ref() else {
3662 return Vec::new();
3663 };
3664
3665 let files = meta.info.files();
3666 if files.is_empty() {
3667 return Vec::new();
3668 }
3669
3670 let piece_length = lengths.piece_length();
3671 let mut result = Vec::with_capacity(files.len());
3672 let mut file_offset = 0u64;
3673
3674 for file_entry in &files {
3675 let file_len = file_entry.length;
3676 if file_len == 0 {
3677 result.push(0);
3678 file_offset += file_len;
3679 continue;
3680 }
3681
3682 let file_end = file_offset + file_len;
3683 let first_piece = (file_offset / piece_length) as u32;
3684 let last_piece = ((file_end - 1) / piece_length) as u32;
3685
3686 let mut downloaded = 0u64;
3687
3688 for p in first_piece..=last_piece {
3689 if !chunk_tracker.has_piece(p) {
3690 continue;
3691 }
3692
3693 let piece_start = lengths.piece_offset(p);
3694 let piece_end = piece_start + u64::from(lengths.piece_size(p));
3695
3696 let overlap_start = piece_start.max(file_offset);
3698 let overlap_end = piece_end.min(file_end);
3699
3700 if overlap_start < overlap_end {
3701 downloaded += overlap_end - overlap_start;
3702 }
3703 }
3704
3705 result.push(downloaded);
3706 file_offset = file_end;
3707 }
3708
3709 result
3710 }
3711
3712 fn v6_retry_delay(&self) -> std::time::Duration {
3715 let base_ms: u64 = 100;
3716 let max_ms: u64 = 5000;
3717 let delay_ms = base_ms
3718 .saturating_mul(
3719 1u64.checked_shl(self.dht_v6_empty_count)
3720 .unwrap_or(u64::MAX),
3721 )
3722 .min(max_ms);
3723 std::time::Duration::from_millis(delay_ms)
3724 }
3725
3726 fn should_retry_v6(&self) -> bool {
3728 let Some(last) = self.dht_v6_last_retry else {
3729 return true; };
3731 last.elapsed() >= self.v6_retry_delay()
3732 }
3733
3734 async fn handle_force_dht_announce(&self) {
3736 if let Some(dht) = self.current_dht()
3737 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
3738 {
3739 warn!("Force DHT v4 announce failed: {e}");
3740 }
3741 if let Some(dht6) = self.current_dht_v6()
3742 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
3743 {
3744 debug!("Force DHT v6 announce failed: {e}");
3745 }
3746 if self.info_hashes.is_hybrid()
3748 && let Some(v2) = self.info_hashes.v2
3749 {
3750 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
3751 if v2_as_v1 != self.info_hash {
3752 if let Some(dht) = self.current_dht()
3753 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
3754 {
3755 debug!("Force DHT v4 dual-swarm announce failed: {e}");
3756 }
3757 if let Some(dht6) = self.current_dht_v6()
3758 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
3759 {
3760 debug!("Force DHT v6 dual-swarm announce failed: {e}");
3761 }
3762 }
3763 }
3764 }
3765
3766 async fn run_dht_requery(&mut self) {
3772 if !self.config.enable_dht {
3773 return;
3774 }
3775
3776 if self.peers.len() > self.config.max_peers.saturating_mul(4) {
3780 return;
3781 }
3782
3783 if self.dht_peers_rx.is_none()
3791 && let Some(dht) = self.current_dht()
3792 {
3793 match dht.get_peers(self.info_hash).await {
3794 Ok(rx) => self.dht_peers_rx = Some(rx),
3795 Err(e) => warn!("DHT v4 re-query failed: {e}"),
3796 }
3797 }
3798
3799 if self.dht_v6_peers_rx.is_none()
3801 && self.dht_v6_empty_count < 30
3802 && self.should_retry_v6()
3803 && let Some(dht6) = self.current_dht_v6()
3804 {
3805 self.dht_v6_last_retry = Some(std::time::Instant::now());
3806 match dht6.get_peers(self.info_hash).await {
3807 Ok(rx) => self.dht_v6_peers_rx = Some(rx),
3808 Err(e) => debug!("DHT v6 re-query failed: {e}"),
3809 }
3810 }
3811
3812 if self.info_hashes.is_hybrid()
3814 && let Some(v2) = self.info_hashes.v2
3815 {
3816 let v2_bytes: [u8; 20] = v2.0[..20]
3817 .try_into()
3818 .expect("Id32 is 32 bytes; first 20 always fit");
3819 let v2_as_v1 = Id20(v2_bytes);
3820
3821 if self.dht_v2_peers_rx.is_none()
3822 && let Some(dht) = self.current_dht()
3823 {
3824 match dht.get_peers(v2_as_v1).await {
3825 Ok(rx) => self.dht_v2_peers_rx = Some(rx),
3826 Err(e) => debug!("DHT v4 v2-swarm re-query failed: {e}"),
3827 }
3828 }
3829 if self.dht_v6_v2_peers_rx.is_none()
3830 && self.dht_v6_empty_count < 30
3831 && self.should_retry_v6()
3832 && let Some(dht6) = self.current_dht_v6()
3833 {
3834 self.dht_v6_last_retry = Some(std::time::Instant::now());
3835 match dht6.get_peers(v2_as_v1).await {
3836 Ok(rx) => self.dht_v6_v2_peers_rx = Some(rx),
3837 Err(e) => debug!("DHT v6 v2-swarm re-query failed: {e}"),
3838 }
3839 }
3840 }
3841
3842 debug!(peers = self.peers.len(), "DHT re-query triggered");
3843 }
3844
3845 async fn handle_read_piece(&self, index: u32) -> crate::Result<Bytes> {
3847 let disk = self
3848 .disk
3849 .as_ref()
3850 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3851 let lengths = self
3852 .lengths
3853 .as_ref()
3854 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3855
3856 let piece_size = lengths.piece_size(index);
3857 if piece_size == 0 {
3858 return Err(crate::Error::InvalidPieceIndex {
3859 index,
3860 num_pieces: lengths.num_pieces(),
3861 });
3862 }
3863
3864 let chunk_size = lengths.chunk_size();
3865 let num_chunks = lengths.chunks_in_piece(index);
3866 let mut buf = bytes::BytesMut::with_capacity(piece_size as usize);
3867
3868 for chunk_idx in 0..num_chunks {
3869 let begin = chunk_idx * chunk_size;
3870 let len = if chunk_idx == num_chunks - 1 {
3871 piece_size - begin
3872 } else {
3873 chunk_size
3874 };
3875 let data = disk
3876 .read_chunk(index, begin, len, DiskJobFlags::empty())
3877 .await
3878 .map_err(crate::Error::Storage)?;
3879 buf.extend_from_slice(&data);
3880 }
3881
3882 Ok(buf.freeze())
3883 }
3884
3885 async fn handle_flush_cache(&self) -> crate::Result<()> {
3887 let disk = self
3888 .disk
3889 .as_ref()
3890 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3891 disk.flush_cache().await.map_err(crate::Error::Storage)
3892 }
3893
3894 fn handle_connect_peer(&mut self, addr: SocketAddr) {
3896 if self.peers.contains_key(&addr) {
3898 return;
3899 }
3900 if let Some(ref ps) = self.peer_states {
3902 ps.add_if_not_seen(addr, PeerSource::Incoming);
3903 }
3904 }
3905
3906 pub(crate) fn fire_tracker_alerts(&self, outcomes: &[crate::tracker_manager::TrackerOutcome]) {
3908 for outcome in outcomes {
3909 match &outcome.result {
3910 Ok(num_peers) => {
3911 post_alert(
3912 &self.alert_tx,
3913 &self.alert_mask,
3914 AlertKind::TrackerReply {
3915 info_hash: self.info_hash,
3916 url: outcome.url.clone(),
3917 num_peers: *num_peers,
3918 },
3919 );
3920 }
3921 Err(msg) => {
3922 post_alert(
3923 &self.alert_tx,
3924 &self.alert_mask,
3925 AlertKind::TrackerError {
3926 info_hash: self.info_hash,
3927 url: outcome.url.clone(),
3928 message: msg.clone(),
3929 },
3930 );
3931 }
3932 }
3933 }
3934 }
3935
3936 pub(crate) fn calculate_left(&self) -> u64 {
3938 match (&self.meta, &self.chunk_tracker) {
3939 (Some(meta), Some(ct)) => {
3940 let total = meta.info.total_length();
3941 let have = u64::from(ct.bitfield().count_ones());
3942 let pieces_total = u64::from(self.num_pieces);
3943 let per_piece = total.checked_div(pieces_total).unwrap_or(0);
3944 total.saturating_sub(have * per_piece)
3945 }
3946 _ => 0,
3947 }
3948 }
3949
3950 pub(crate) async fn shutdown_peers(&mut self) {
3951 let left = self.calculate_left();
3953 let _ = tokio::time::timeout(
3954 std::time::Duration::from_secs(3),
3955 self.tracker_manager
3956 .announce_stopped(self.uploaded, self.downloaded, left),
3957 )
3958 .await;
3959
3960 for peer in self.peers.values() {
3962 let _ = peer.cmd_tx.try_send(PeerCommand::Shutdown);
3963 }
3964 }
3965
3966 pub(crate) async fn handle_piece_data(
3969 &mut self,
3970 peer_addr: SocketAddr,
3971 index: u32,
3972 begin: u32,
3973 data: Bytes,
3974 ) {
3975 if let Some(ref ct) = self.chunk_tracker
3979 && ct.has_chunk(index, begin)
3980 {
3981 self.total_download += data.len() as u64 + 13;
3982 if let Some(peer) = self.peers.get_mut(&peer_addr) {
3986 peer.pending_requests.remove(index, begin);
3987 }
3988 if self.end_game.is_active() {
3992 self.end_game.block_received(index, begin, peer_addr);
3993 }
3994 return;
3996 }
3997
3998 let data_len = data.len();
3999
4000 if let Some(ref disk) = self.disk {
4002 disk.write_block_deferred(index, begin, data);
4003 }
4004
4005 self.downloaded += data_len as u64;
4006 self.total_download += data_len as u64 + 13; self.last_download = now_unix();
4008 self.need_save_resume = true;
4009
4010 if let Some(slab_idx) = self.peer_slab.slot_of(&peer_addr)
4012 && self.piece_owner.get(index as usize) == Some(&None)
4013 {
4014 self.piece_owner[index as usize] = Some(slab_idx);
4015 if self.inflight_started.get(index as usize) == Some(&None) {
4017 self.inflight_started[index as usize] = Some(Instant::now());
4018 }
4019 if let (Some(sc), Some(bm)) = (&self.steal_candidates, &self.block_maps)
4021 && let Some(lengths) = &self.lengths
4022 {
4023 let total_blocks = lengths.chunks_in_piece(index);
4024 if bm.next_unrequested(index, total_blocks).is_some() {
4025 sc.push(index);
4026 }
4027 }
4028 }
4029
4030 self.piece_contributors
4032 .entry(index)
4033 .or_default()
4034 .insert(peer_addr.ip());
4035
4036 let now = std::time::Instant::now();
4037 if let Some(peer) = self.peers.get_mut(&peer_addr) {
4038 peer.pending_requests.remove(index, begin);
4039 peer.download_bytes_window += data_len as u64;
4040 peer.download_bytes_total += data_len as u64;
4041 peer.pipeline
4042 .block_received(index, begin, data_len as u32, now);
4043 peer.last_data_received = Some(now);
4044 if peer.snubbed {
4046 peer.snubbed = false;
4047 }
4048 }
4049 if self.end_game.is_active() {
4054 let cancels = self.end_game.block_received(index, begin, peer_addr);
4055 for (cancel_addr, ci, cb, cl) in cancels {
4056 if let Some(cancel_peer) = self.peers.get_mut(&cancel_addr) {
4057 let _ = cancel_peer.cmd_tx.try_send(PeerCommand::Cancel {
4058 index: ci,
4059 begin: cb,
4060 length: cl,
4061 });
4062 cancel_peer.pending_requests.remove(ci, cb);
4063 }
4064 }
4065 }
4066
4067 let piece_complete = if let Some(ref mut ct) = self.chunk_tracker {
4069 ct.chunk_received(index, begin)
4070 } else {
4071 false
4072 };
4073
4074 if piece_complete && !self.pending_verify.contains(&index) {
4075 if self.config.predictive_piece_announce_ms > 0
4077 && !self.predictive_have_sent.contains(&index)
4078 {
4079 self.predictive_have_sent.insert(index);
4080 let _ = self.have_broadcast_tx.send(index);
4081 }
4082
4083 if let Some(ref disk) = self.disk {
4086 disk.flush_piece_writes(index).await;
4087 }
4088
4089 match self.version {
4090 irontide_core::TorrentVersion::V1Only => {
4091 if let Some(ref disk) = self.disk
4093 && let Some(expected) = self
4094 .meta
4095 .as_ref()
4096 .and_then(|m| m.info.piece_hash(index as usize))
4097 {
4098 self.pending_verify.insert(index);
4099 let generation = self
4100 .piece_generations
4101 .get(index as usize)
4102 .copied()
4103 .unwrap_or(0);
4104 disk.enqueue_verify(index, expected, generation, &self.verify_result_tx);
4105 }
4106 }
4107 irontide_core::TorrentVersion::V2Only => {
4108 self.verify_and_mark_piece_v2(index).await;
4110 }
4111 irontide_core::TorrentVersion::Hybrid => {
4112 self.verify_and_mark_piece_hybrid(index).await;
4114 }
4115 }
4116 }
4117
4118 if self.end_game.is_active() {
4121 self.request_end_game_block(peer_addr).await;
4122 }
4123 }
4124
4125 pub(crate) async fn handle_piece_blocks_batch(
4130 &mut self,
4131 peer_addr: SocketAddr,
4132 blocks: Vec<crate::types::BlockEntry>,
4133 ) {
4134 for block in &blocks {
4135 self.process_block_completion(peer_addr, block.index, block.begin, block.length)
4136 .await;
4137 }
4138 }
4139
4140 fn handle_open_file(
4141 &mut self,
4142 file_index: usize,
4143 ) -> crate::Result<crate::streaming::FileStreamHandle> {
4144 let meta = self
4145 .meta
4146 .as_ref()
4147 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4148 let files = meta.info.files();
4149 if file_index >= files.len() {
4150 return Err(crate::Error::InvalidFileIndex {
4151 index: file_index,
4152 count: files.len(),
4153 });
4154 }
4155 if self.file_priorities.get(file_index).copied() == Some(FilePriority::Skip) {
4156 return Err(crate::Error::FileSkipped { index: file_index });
4157 }
4158
4159 let lengths = self
4160 .lengths
4161 .as_ref()
4162 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4163 let disk = self
4164 .disk
4165 .as_ref()
4166 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4167
4168 let mut file_offset = 0u64;
4170 for f in &files[..file_index] {
4171 file_offset += f.length;
4172 }
4173 let file_length = files[file_index].length;
4174
4175 let (cursor_tx, cursor_rx) = tokio::sync::watch::channel(0u64);
4176
4177 let permit = self
4178 .stream_read_semaphore
4179 .clone()
4180 .try_acquire_owned()
4181 .map_err(|_| crate::Error::Connection("too many concurrent stream readers".into()))?;
4182
4183 self.streaming_cursors
4185 .push(crate::streaming::StreamingCursor {
4186 file_index,
4187 file_offset,
4188 cursor_piece: (file_offset / lengths.piece_length()) as u32,
4189 readahead_pieces: self.config.readahead_pieces,
4190 cursor_rx,
4191 });
4192
4193 Ok(crate::streaming::FileStreamHandle {
4194 disk: disk.clone(),
4195 lengths: lengths.clone(),
4196 file_index,
4197 file_offset,
4198 file_length,
4199 cursor_tx,
4200 piece_ready_rx: self.piece_ready_tx.subscribe(),
4201 have: self.have_watch_rx.clone(),
4202 read_permit: permit,
4203 })
4204 }
4205
4206 async fn suggest_cached_pieces(&mut self) {
4208 if !self.config.suggest_mode {
4209 return;
4210 }
4211 let disk = match self.disk {
4212 Some(ref d) => d.clone(),
4213 None => return,
4214 };
4215 let cached = disk.cached_pieces().await;
4216 if cached.is_empty() {
4217 return;
4218 }
4219 let max_suggest = self.config.max_suggest_pieces;
4220 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4221 for peer_addr in peer_addrs {
4222 let already_suggested = self.suggested_to_peers.entry(peer_addr).or_default();
4223 let peer_has_piece = |piece: u32| -> bool {
4224 self.peers
4225 .get(&peer_addr)
4226 .is_some_and(|p| p.bitfield.get(piece))
4227 };
4228 let mut sent = 0;
4229 for &piece in &cached {
4230 if sent >= max_suggest {
4231 break;
4232 }
4233 if peer_has_piece(piece) {
4234 continue;
4235 }
4236 if already_suggested.contains(&piece) {
4237 continue;
4238 }
4239 if let Some(peer) = self.peers.get(&peer_addr) {
4240 let _ = peer.cmd_tx.try_send(PeerCommand::SuggestPiece(piece));
4241 already_suggested.insert(piece);
4242 sent += 1;
4243 }
4244 }
4245 }
4246 }
4247
4248 async fn handle_pre_resolved_metadata(&mut self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
4254 if self.state != TorrentState::FetchingMetadata {
4256 debug!(
4257 info_hash = %self.info_hash,
4258 state = ?self.state,
4259 "ignoring pre-resolved metadata: already past FetchingMetadata"
4260 );
4261 return;
4262 }
4263
4264 debug!(
4265 info_hash = %self.info_hash,
4266 info_bytes_len = info_bytes.len(),
4267 num_peers = peers.len(),
4268 "received pre-resolved metadata from background resolver"
4269 );
4270
4271 if let Some(ref mut dl) = self.metadata_downloader {
4273 dl.set_total_size(info_bytes.len() as u64);
4275
4276 let piece_size: usize = 16384;
4280 let num_pieces = info_bytes.len().div_ceil(piece_size);
4281 for i in 0..num_pieces {
4282 let start = i * piece_size;
4283 let end = (start + piece_size).min(info_bytes.len());
4284 let data = bytes::Bytes::copy_from_slice(&info_bytes[start..end]);
4285 dl.piece_received(i as u32, data);
4286 }
4287 }
4288
4289 self.try_assemble_metadata().await;
4292
4293 if !peers.is_empty() {
4295 self.handle_add_peers(peers, crate::peer_state::PeerSource::Dht);
4296 }
4297 }
4298
4299 pub(crate) async fn try_assemble_metadata(&mut self) {
4300 let assembled = if let Some(ref dl) = self.metadata_downloader {
4301 dl.assemble_and_verify()
4302 } else {
4303 return;
4304 };
4305
4306 match assembled {
4307 Ok(info_bytes) => {
4308 let mut torrent_bytes = b"d4:info".to_vec();
4314 torrent_bytes.extend_from_slice(&info_bytes);
4315 torrent_bytes.push(b'e');
4316
4317 match torrent_from_bytes(&torrent_bytes) {
4318 Ok(meta) => {
4319 let num_pieces = meta.info.num_pieces() as u32;
4320 let lengths = Lengths::new(
4321 meta.info.total_length(),
4322 meta.info.piece_length,
4323 DEFAULT_CHUNK_SIZE,
4324 );
4325
4326 let files = meta.info.files();
4328 let file_paths: Vec<std::path::PathBuf> = files
4329 .iter()
4330 .map(|f| f.path.iter().collect::<std::path::PathBuf>())
4331 .collect();
4332 let file_lengths_vec: Vec<u64> = files.iter().map(|f| f.length).collect();
4333 let prealloc_mode = self.config.preallocate_mode.unwrap_or_else(|| {
4334 irontide_storage::PreallocateMode::from(
4335 self.config.storage_mode == irontide_core::StorageMode::Full,
4336 )
4337 });
4338 let storage: Arc<dyn TorrentStorage> =
4339 match irontide_storage::FilesystemStorage::new(
4340 &self.config.download_dir,
4341 file_paths,
4342 file_lengths_vec,
4343 lengths.clone(),
4344 None,
4345 prealloc_mode,
4346 self.config.filesystem_direct_io,
4347 ) {
4348 Ok(s) => Arc::new(s),
4349 Err(e) => {
4350 warn!(
4351 "failed to create filesystem storage: {e}, falling back to memory"
4352 );
4353 Arc::new(MemoryStorage::new(lengths.clone()))
4354 }
4355 };
4356 let mut disk_handle = self
4357 .disk_manager
4358 .register_torrent(self.info_hash, storage)
4359 .await;
4360
4361 self.chunk_tracker = Some(ChunkTracker::new(lengths.clone()));
4362 self.lengths = Some(lengths);
4363 self.num_pieces = num_pieces;
4364 self.piece_generations = vec![0u64; num_pieces as usize];
4366 let (hash_tx, hash_rx) = tokio::sync::mpsc::channel(64);
4367 self.hash_result_tx = hash_tx;
4368 self.hash_result_rx = hash_rx;
4369 if let Some(ref pool) = self.hash_pool_ref {
4372 disk_handle.set_hash_pool(pool.clone());
4373 disk_handle.set_hash_result_tx(self.hash_result_tx.clone());
4374 }
4375 self.disk = Some(disk_handle);
4376 for peer in self.peers.values() {
4379 let _ = peer
4380 .cmd_tx
4381 .try_send(PeerCommand::UpdateNumPieces(num_pieces));
4382 }
4383 let file_lengths: Vec<u64> =
4384 meta.info.files().iter().map(|f| f.length).collect();
4385 let mut meta = meta;
4386 meta.info_bytes = Some(Bytes::from(info_bytes));
4387 self.meta = Some(meta);
4388
4389 if let (Some(meta), Some(lengths)) = (&self.meta, &self.lengths) {
4391 self.cached_files = Some(build_cached_file_info(meta, lengths));
4392 }
4393
4394 self.file_priorities = vec![FilePriority::Normal; file_lengths.len()];
4395
4396 if let Some(ref selections) = self.magnet_selected_files {
4398 self.file_priorities = irontide_core::FileSelection::to_priorities(
4399 selections,
4400 file_lengths.len(),
4401 );
4402 self.magnet_selected_files = None;
4403 }
4404
4405 self.wanted_pieces = crate::piece_selector::build_wanted_pieces(
4406 &self.file_priorities,
4407 &file_lengths,
4408 self.lengths.as_ref().unwrap(),
4409 );
4410 if self.config.share_mode {
4411 self.transition_state(TorrentState::Sharing);
4412 } else {
4413 self.transition_state(TorrentState::Downloading);
4414 }
4415 self.metadata_downloader = None;
4416
4417 if let Some(ref meta) = self.meta {
4419 self.tracker_manager
4420 .set_metadata_filtered(meta, self.config.url_security);
4421 }
4422
4423 if let Ok(detected) = irontide_core::torrent_from_bytes_any(&torrent_bytes)
4426 {
4427 let new_version = detected.version();
4428 if new_version != irontide_core::TorrentVersion::V1Only {
4429 let new_hashes = detected.info_hashes();
4430 self.version = new_version;
4431 self.info_hashes = new_hashes.clone();
4432 self.tracker_manager.set_info_hashes(new_hashes.clone());
4433 if let Some(v2_meta) = detected.as_v2() {
4434 self.meta_v2 = Some(v2_meta.clone());
4435 }
4436 if new_hashes.is_hybrid()
4438 && let Some(v2) = new_hashes.v2
4439 {
4440 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
4441 if v2_as_v1 != self.info_hash {
4442 if self.dht_v2_peers_rx.is_none()
4443 && let Some(dht) = self.current_dht()
4444 && let Ok(rx) = dht.get_peers(v2_as_v1).await
4445 {
4446 self.dht_v2_peers_rx = Some(rx);
4447 }
4448 if self.dht_v6_v2_peers_rx.is_none()
4449 && self.dht_v6_empty_count < 30
4450 && self.should_retry_v6()
4451 && let Some(dht6) = self.current_dht_v6()
4452 && let Ok(rx) = dht6.get_peers(v2_as_v1).await
4453 {
4454 self.dht_v6_last_retry =
4455 Some(std::time::Instant::now());
4456 self.dht_v6_v2_peers_rx = Some(rx);
4457 }
4458 }
4459 }
4460 }
4461 }
4462
4463 let name = self
4464 .meta
4465 .as_ref()
4466 .map(|m| m.info.name.clone())
4467 .unwrap_or_default();
4468 post_alert(
4469 &self.alert_tx,
4470 &self.alert_mask,
4471 AlertKind::MetadataReceived {
4472 info_hash: self.info_hash,
4473 name,
4474 },
4475 );
4476 info!("metadata assembled, switching to Downloading");
4477
4478 if let Some(ct) = &self.chunk_tracker {
4480 let atomic_states = Arc::new(AtomicPieceStates::new(
4481 self.num_pieces,
4482 ct.bitfield(),
4483 &self.wanted_pieces,
4484 ));
4485 self.atomic_states = Some(Arc::clone(&atomic_states));
4486 self.piece_owner = vec![None; self.num_pieces as usize];
4487 self.inflight_started = vec![None; self.num_pieces as usize];
4489 self.max_in_flight = self.config.max_in_flight_pieces;
4490
4491 if self.config.use_block_stealing {
4493 if let Some(ref lengths) = self.lengths {
4494 self.block_maps =
4495 Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
4496 }
4497 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
4498 }
4499 self.piece_write_guards = Some(Arc::new(
4501 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
4502 ));
4503
4504 self.piece_tracker = Some(PieceTracker::new(
4506 self.num_pieces,
4507 ct.bitfield(),
4508 &self.wanted_pieces,
4509 ));
4510 if let Some(ref cached) = self.cached_files {
4511 let file_piece_ranges: Vec<(u32, u32)> = cached
4512 .entries
4513 .iter()
4514 .map(|e| (e.first_piece, e.last_piece))
4515 .collect();
4516 let om = Arc::new(PieceOrderMap::build(
4517 &self.file_priorities,
4518 &file_piece_ranges,
4519 self.num_pieces,
4520 0,
4521 ));
4522 self.order_map_tx.send_replace(om);
4523 }
4524
4525 let notify = Arc::new(tokio::sync::Notify::new());
4526 self.reservation_notify = Some(notify);
4527 }
4528
4529 self.spawn_web_seeds();
4531 self.assign_pieces_to_web_seeds();
4532
4533 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4536 info!(
4537 connected_peers = peer_addrs.len(),
4538 "kick-starting piece requests for pre-connected peers"
4539 );
4540 for addr in peer_addrs {
4541 let has_bitfield =
4542 self.peers.get(&addr).map_or(0, |p| p.bitfield.count_ones());
4543 let is_choking = self.peers.get(&addr).is_none_or(|p| p.peer_choking);
4544 debug!(%addr, has_bitfield, is_choking, "post-metadata peer state");
4545 self.maybe_express_interest(addr).await;
4546 if let Some(peer) = self.peers.get(&addr)
4547 && peer.bitfield.count_ones() > 0
4548 {
4549 let _slot = self.peer_slab.insert(addr);
4550 }
4551 }
4552 self.recalc_max_in_flight();
4553 if !self.user_seed_mode
4557 && let Some(notify) = &self.reservation_notify
4558 && let Some(ref lengths) = self.lengths
4559 {
4560 for peer in self.peers.values() {
4561 let _ = peer.cmd_tx.try_send(PeerCommand::StartRequesting {
4562 piece_notify: Arc::clone(notify),
4563 disk_handle: self.disk.clone(),
4564 write_error_tx: self.write_error_tx.clone(),
4565 lengths: lengths.clone(),
4566 });
4567 }
4568 }
4569 }
4570 Err(e) => {
4571 warn!("failed to parse assembled metadata: {e}");
4572 post_alert(
4573 &self.alert_tx,
4574 &self.alert_mask,
4575 AlertKind::MetadataFailed {
4576 info_hash: self.info_hash,
4577 },
4578 );
4579 }
4580 }
4581 }
4582 Err(e) => {
4583 warn!("metadata assembly failed: {e}");
4584 post_alert(
4585 &self.alert_tx,
4586 &self.alert_mask,
4587 AlertKind::MetadataFailed {
4588 info_hash: self.info_hash,
4589 },
4590 );
4591 }
4592 }
4593 }
4594
4595 fn spawn_web_seeds(&mut self) {
4598 if !self.config.enable_web_seed {
4599 return;
4600 }
4601 let Some(meta) = &self.meta else { return };
4602 let lengths = match &self.lengths {
4603 Some(l) => l.clone(),
4604 None => return,
4605 };
4606
4607 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
4608 let file_map = irontide_storage::FileMap::new(file_lengths, lengths.clone());
4609
4610 for url in &meta.url_list {
4612 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4613 continue;
4614 }
4615 if self.web_seeds.len() >= self.config.max_web_seeds {
4616 break;
4617 }
4618
4619 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4621 warn!(%url, %e, "web seed URL rejected by security policy");
4622 continue;
4623 }
4624
4625 let url_builder = if meta.info.length.is_some() {
4626 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone())
4627 } else {
4628 let file_paths: Vec<String> = meta
4629 .info
4630 .files()
4631 .iter()
4632 .map(|f| f.path[1..].join("/")) .collect();
4634 crate::web_seed::WebSeedUrlBuilder::multi(
4635 url.clone(),
4636 meta.info.name.clone(),
4637 file_paths,
4638 )
4639 };
4640
4641 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4642 let initial_downloaded = self
4643 .web_seed_stats
4644 .get(url)
4645 .map_or(0, |s| s.downloaded_bytes);
4646 let task = crate::web_seed::WebSeedTask::new(
4647 url.clone(),
4648 crate::web_seed::WebSeedMode::GetRight,
4649 url_builder,
4650 lengths.clone(),
4651 file_map.clone(),
4652 self.info_hash,
4653 cmd_rx,
4654 self.event_tx.clone(),
4655 self.config.url_security,
4656 self.config.web_seed_progress_throttle_ms,
4657 initial_downloaded,
4658 self.config.web_seed_retry_base_secs,
4659 self.config.web_seed_retry_factor,
4660 self.config.web_seed_retry_cap_secs,
4661 self.config.web_seed_max_failures,
4662 );
4663 tokio::spawn(task.run());
4664 self.web_seeds.insert(url.clone(), cmd_tx);
4665 debug!(url, "spawned BEP 19 web seed");
4666 }
4667
4668 for url in &meta.httpseeds {
4670 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4671 continue;
4672 }
4673 if self.web_seeds.len() >= self.config.max_web_seeds {
4674 break;
4675 }
4676
4677 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4679 warn!(%url, %e, "web seed URL rejected by security policy");
4680 continue;
4681 }
4682
4683 let url_builder =
4685 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone());
4686
4687 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4688 let initial_downloaded = self
4689 .web_seed_stats
4690 .get(url)
4691 .map_or(0, |s| s.downloaded_bytes);
4692 let task = crate::web_seed::WebSeedTask::new(
4693 url.clone(),
4694 crate::web_seed::WebSeedMode::Hoffman,
4695 url_builder,
4696 lengths.clone(),
4697 file_map.clone(),
4698 self.info_hash,
4699 cmd_rx,
4700 self.event_tx.clone(),
4701 self.config.url_security,
4702 self.config.web_seed_progress_throttle_ms,
4703 initial_downloaded,
4704 self.config.web_seed_retry_base_secs,
4705 self.config.web_seed_retry_factor,
4706 self.config.web_seed_retry_cap_secs,
4707 self.config.web_seed_max_failures,
4708 );
4709 tokio::spawn(task.run());
4710 self.web_seeds.insert(url.clone(), cmd_tx);
4711 debug!(url, "spawned BEP 17 web seed");
4712 }
4713 }
4714
4715 pub(crate) fn assign_pieces_to_web_seeds(&mut self) {
4716 if self.state != TorrentState::Downloading || self.end_game.is_active() {
4717 return;
4718 }
4719
4720 let active_urls: HashSet<&String> = self.web_seed_in_flight.values().collect();
4722 let idle_urls: Vec<String> = self
4723 .web_seeds
4724 .keys()
4725 .filter(|u| !active_urls.contains(u))
4726 .cloned()
4727 .collect();
4728
4729 let Some(ct) = &self.chunk_tracker else {
4730 return;
4731 };
4732
4733 for url in idle_urls {
4734 let piece = (0..self.num_pieces).find(|&i| {
4737 !ct.has_piece(i)
4738 && !self
4739 .piece_owner
4740 .get(i as usize)
4741 .is_some_and(std::option::Option::is_some)
4742 && !self.web_seed_in_flight.contains_key(&i)
4743 && self.wanted_pieces.get(i)
4744 });
4745
4746 if let Some(piece) = piece
4747 && let Some(cmd_tx) = self.web_seeds.get(&url)
4748 {
4749 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::FetchPiece(piece));
4750 self.web_seed_in_flight.insert(piece, url);
4751 }
4752 }
4753 }
4754
4755 pub(crate) async fn handle_web_seed_piece_data(
4756 &mut self,
4757 url: String,
4758 index: u32,
4759 data: Bytes,
4760 ) {
4761 self.web_seed_in_flight.remove(&index);
4762
4763 if let Some(ref ct) = self.chunk_tracker
4765 && ct.has_piece(index)
4766 {
4767 self.assign_pieces_to_web_seeds();
4768 return;
4769 }
4770
4771 if let Some(ref disk) = self.disk
4773 && let Err(e) = disk
4774 .write_chunk(index, 0, data.clone(), DiskJobFlags::FLUSH_PIECE)
4775 .await
4776 {
4777 warn!(index, "web seed: failed to write piece: {e}");
4778 self.assign_pieces_to_web_seeds();
4779 return;
4780 }
4781
4782 if let Some(ref mut ct) = self.chunk_tracker
4784 && let Some(ref lengths) = self.lengths
4785 {
4786 let num_chunks = lengths.chunks_in_piece(index);
4787 for chunk_idx in 0..num_chunks {
4788 if let Some((begin, _len)) = lengths.chunk_info(index, chunk_idx) {
4789 ct.chunk_received(index, begin);
4790 }
4791 }
4792 }
4793
4794 self.downloaded += data.len() as u64;
4795 self.total_download += data.len() as u64 + 13; self.last_download = now_unix();
4797 self.need_save_resume = true;
4798
4799 self.verify_and_mark_piece(index).await;
4801
4802 if let Some(ref ct) = self.chunk_tracker
4804 && !ct.has_piece(index)
4805 {
4806 self.ban_web_seed(&url);
4807 return;
4808 }
4809
4810 self.assign_pieces_to_web_seeds();
4811 }
4812
4813 pub(crate) fn handle_web_seed_error(&mut self, url: &str, piece: u32, message: &str) {
4814 self.web_seed_in_flight.remove(&piece);
4815 warn!(%url, piece, %message, "web seed error");
4816 self.assign_pieces_to_web_seeds();
4817 }
4818
4819 pub(crate) fn handle_web_seed_progress(
4827 &mut self,
4828 url: &str,
4829 bytes: u64,
4830 rate_bps: u64,
4831 error: Option<String>,
4832 ) {
4833 let now_unix = std::time::SystemTime::now()
4834 .duration_since(std::time::UNIX_EPOCH)
4835 .map_or(0, |d| d.as_secs());
4836 let entry = self
4837 .web_seed_stats
4838 .entry(url.to_owned())
4839 .or_insert_with(|| irontide_core::WebSeedStats {
4840 url: url.to_owned(),
4841 ..Default::default()
4842 });
4843 entry.downloaded_bytes = bytes;
4844 entry.last_rate_bps = rate_bps;
4845 entry.last_attempt_unix_secs = now_unix;
4846 if let Some(msg) = error {
4847 entry.state = irontide_core::WebSeedState::Errored;
4848 entry.last_error = Some(msg);
4849 entry.consecutive_failures = entry.consecutive_failures.saturating_add(1);
4850 let attempt = entry.consecutive_failures.saturating_sub(1);
4852 let secs = self
4853 .config
4854 .web_seed_retry_base_secs
4855 .saturating_mul(self.config.web_seed_retry_factor.saturating_pow(attempt))
4856 .min(self.config.web_seed_retry_cap_secs);
4857 entry.next_retry_unix_secs = Some(now_unix + secs);
4858 } else {
4859 entry.state = irontide_core::WebSeedState::Active;
4860 entry.consecutive_failures = 0;
4861 entry.next_retry_unix_secs = None;
4862 }
4863 self.need_save_resume = true;
4864 }
4865
4866 pub(crate) fn ban_web_seed(&mut self, url: &str) {
4867 warn!(%url, "banning web seed due to hash failure");
4868 self.banned_web_seeds.insert(url.to_owned());
4869
4870 if let Some(cmd_tx) = self.web_seeds.remove(url) {
4872 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::Shutdown);
4873 }
4874
4875 self.web_seed_in_flight.retain(|_, v| v != url);
4877
4878 post_alert(
4879 &self.alert_tx,
4880 &self.alert_mask,
4881 AlertKind::WebSeedBanned {
4882 info_hash: self.info_hash,
4883 url: url.to_owned(),
4884 },
4885 );
4886 }
4887
4888 async fn shutdown_web_seeds(&mut self) {
4889 for (_, cmd_tx) in self.web_seeds.drain() {
4890 let _ = cmd_tx.send(crate::web_seed::WebSeedCommand::Shutdown).await;
4891 }
4892 self.web_seed_in_flight.clear();
4893 }
4894
4895 fn refresh_peer_rates(&mut self) {
4897 self.cached_peer_rates.clear();
4898 self.cached_peer_rates.reserve(self.peers.len());
4899 for (&addr, p) in &self.peers {
4900 self.cached_peer_rates.insert(addr, p.pipeline.ewma_rate());
4901 }
4902 }
4903
4904 fn update_peer_rates(&mut self) {
4907 for peer in self.peers.values_mut() {
4908 peer.download_rate = peer.download_bytes_window / 2;
4909 peer.upload_rate = peer.upload_bytes_window / 2;
4910 peer.download_bytes_window = 0;
4911 peer.upload_bytes_window = 0;
4912 }
4913
4914 let aggregate_download: u64 = self.peers.values().map(|p| p.download_rate).sum();
4916 if aggregate_download > self.peak_download_rate {
4917 self.peak_download_rate = aggregate_download;
4918 }
4919 }
4920
4921 async fn run_choker(&mut self) {
4922 let peer_infos: Vec<ChokerPeerInfo> = self
4923 .peers
4924 .values()
4925 .map(|p| ChokerPeerInfo {
4926 addr: p.addr,
4927 download_rate: p.download_rate,
4928 upload_rate: p.upload_rate,
4929 interested: p.peer_interested,
4930 upload_only: p.upload_only,
4931 is_seed: p.upload_only
4932 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
4933 })
4934 .collect();
4935
4936 let decision = self.choker.decide(&peer_infos);
4937
4938 for addr in &decision.to_unchoke {
4939 if let Some(peer) = self.peers.get_mut(addr)
4940 && peer.am_choking
4941 {
4942 peer.am_choking = false;
4943 if peer.am_unchoke_started_at.is_none() {
4945 peer.am_unchoke_started_at = Some(Instant::now());
4946 }
4947 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(false));
4948 }
4949 }
4950
4951 for addr in &decision.to_choke {
4952 if let Some(peer) = self.peers.get_mut(addr)
4953 && !peer.am_choking
4954 {
4955 if peer.supports_fast {
4956 let pending: Vec<(u32, u32, u32)> = peer.incoming_requests.drain(..).collect();
4957 for (index, begin, length) in pending {
4958 let _ = peer.cmd_tx.try_send(PeerCommand::RejectRequest {
4959 index,
4960 begin,
4961 length,
4962 });
4963 }
4964 }
4965 peer.am_choking = true;
4966 if let Some(start) = peer.am_unchoke_started_at.take() {
4968 peer.unchoke_duration_total += start.elapsed();
4969 }
4970 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(true));
4971 }
4972 }
4973
4974 self.serve_incoming_requests().await;
4976
4977 if self.state == TorrentState::Downloading {
4982 let zombie_threshold = Duration::from_secs(30);
4983 let zombies: Vec<SocketAddr> = self
4984 .peers
4985 .values()
4986 .filter(|p| {
4987 p.bitfield.count_ones() == 0 && p.connected_at.elapsed() > zombie_threshold
4988 })
4989 .map(|p| p.addr)
4990 .collect();
4991
4992 for &addr in &zombies {
4993 debug!(%addr, "disconnecting zombie peer (empty bitfield after 30s)");
4994 self.disconnect_peer(addr, "zombie peer (empty bitfield)");
4995 }
4996 if !zombies.is_empty() {
4997 self.recalc_max_in_flight();
4998 }
4999 }
5000 }
5001
5002 fn rotate_optimistic(&mut self) {
5003 let peer_infos: Vec<ChokerPeerInfo> = self
5004 .peers
5005 .values()
5006 .map(|p| ChokerPeerInfo {
5007 addr: p.addr,
5008 download_rate: p.download_rate,
5009 upload_rate: p.upload_rate,
5010 interested: p.peer_interested,
5011 upload_only: p.upload_only,
5012 is_seed: p.upload_only
5013 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
5014 })
5015 .collect();
5016
5017 self.choker.rotate_optimistic(&peer_infos);
5018 }
5019
5020 fn handle_i2p_incoming(&mut self, stream: crate::i2p::SamStream) {
5026 if self.peers.len() >= self.effective_max_connections() {
5027 return;
5028 }
5029
5030 let synthetic_addr = self.next_i2p_synthetic_addr();
5031
5032 let remote_dest = stream.remote_destination().clone();
5033 let dest_preview = {
5034 let b64 = remote_dest.to_base64();
5035 if b64.len() >= 8 {
5036 b64[..8].to_string()
5037 } else {
5038 b64
5039 }
5040 };
5041 self.i2p_destinations.insert(synthetic_addr, remote_dest);
5042 let tcp_stream = stream.into_inner();
5043
5044 self.spawn_peer_from_stream(synthetic_addr, tcp_stream);
5045
5046 debug!(dest = %dest_preview, addr = %synthetic_addr, "accepted I2P peer");
5047 }
5048
5049 #[allow(dead_code)] fn add_i2p_peer(
5052 &mut self,
5053 dest: crate::i2p::I2pDestination,
5054 source: PeerSource,
5055 ) -> Option<SocketAddr> {
5056 if self.i2p_destinations.values().any(|d| d == &dest) {
5058 return None;
5059 }
5060 let addr = self.next_i2p_synthetic_addr();
5061 self.i2p_destinations.insert(addr, dest);
5062 if let Some(ref ps) = self.peer_states {
5064 ps.add_if_not_seen(addr, source);
5065 }
5066 Some(addr)
5067 }
5068
5069 fn next_i2p_synthetic_addr(&mut self) -> SocketAddr {
5075 self.i2p_peer_counter = self.i2p_peer_counter.wrapping_add(1);
5076 let a = ((self.i2p_peer_counter >> 16) & 0x0F) as u8 | 0xF0;
5077 let b = ((self.i2p_peer_counter >> 8) & 0xFF) as u8;
5078 let c = (self.i2p_peer_counter & 0xFF) as u8;
5079 SocketAddr::new(
5080 std::net::IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, 1)),
5081 (self.i2p_peer_counter & 0xFFFF) as u16,
5082 )
5083 }
5084}
5085
5086pub(crate) fn is_i2p_synthetic_addr(addr: &SocketAddr) -> bool {
5088 match addr {
5089 SocketAddr::V4(v4) => v4.ip().octets()[0] & 0xF0 == 0xF0,
5090 SocketAddr::V6(_) => false,
5091 }
5092}
5093
5094async fn accept_incoming(
5097 listener: &mut Option<Box<dyn crate::transport::TransportListener>>,
5098) -> std::io::Result<(crate::transport::BoxedStream, SocketAddr)> {
5099 match listener {
5100 Some(l) => l.accept().await,
5101 None => std::future::pending().await,
5102 }
5103}
5104
5105async fn accept_i2p(
5108 rx: &mut Option<mpsc::Receiver<crate::i2p::SamStream>>,
5109) -> Option<crate::i2p::SamStream> {
5110 match rx {
5111 Some(rx) => rx.recv().await,
5112 None => std::future::pending().await,
5113 }
5114}
5115
5116pub(crate) fn serve_hashes(
5127 meta_v2: Option<&irontide_core::TorrentMetaV2>,
5128 version: irontide_core::TorrentVersion,
5129 lengths: Option<&Lengths>,
5130 request: &irontide_core::HashRequest,
5131) -> Option<Vec<irontide_core::Id32>> {
5132 let meta_v2 = match meta_v2 {
5134 Some(m) if version != irontide_core::TorrentVersion::V1Only => m,
5135 _ => return None,
5136 };
5137
5138 let piece_hashes = meta_v2.file_piece_hashes(&request.file_root)?;
5140
5141 let lengths = lengths?;
5143
5144 let blocks_per_piece = (meta_v2.info.piece_length / u64::from(lengths.chunk_size())) as u32;
5149 let num_pieces = piece_hashes.len() as u32;
5150 let num_blocks = num_pieces.saturating_mul(blocks_per_piece);
5151
5152 if !irontide_core::validate_hash_request(request, num_blocks, num_pieces) {
5153 return None;
5154 }
5155
5156 let piece_layer_base = blocks_per_piece.trailing_zeros();
5159 if request.base != piece_layer_base {
5160 return None;
5161 }
5162
5163 let start = request.index as usize;
5165 let end = (start + request.count as usize).min(piece_hashes.len());
5166 let mut hashes: Vec<irontide_core::Id32> = piece_hashes[start..end].to_vec();
5167
5168 if request.proof_layers > 0 && !piece_hashes.is_empty() {
5176 let tree = irontide_core::MerkleTree::from_leaves(&piece_hashes);
5177 let full_proof = tree.proof_path(start);
5178 let subtree_depth = if request.count > 1 {
5180 (request.count as usize)
5181 .next_power_of_two()
5182 .trailing_zeros() as usize
5183 } else {
5184 0
5185 };
5186 let available = full_proof.len().saturating_sub(subtree_depth);
5187 let proof_count = (request.proof_layers as usize).min(available);
5188 hashes.extend_from_slice(&full_proof[subtree_depth..subtree_depth + proof_count]);
5189 }
5190
5191 Some(hashes)
5192}
5193
5194#[cfg(test)]
5199impl TorrentActor {
5200 pub(crate) fn for_throttle_test(num_pieces: u32, _throttle_ms: u64) -> Self {
5215 use irontide_storage::Bitfield;
5216
5217 let config = TorrentConfig {
5218 ..TorrentConfig::default()
5219 };
5220
5221 let info_hash = Id20([0u8; 20]);
5222 let our_peer_id = Id20([0u8; 20]);
5223
5224 let (_cmd_tx, cmd_rx) = mpsc::channel(1);
5225 let (event_tx, event_rx) = mpsc::channel(1);
5226 let (write_error_tx, write_error_rx) = mpsc::channel(1);
5227 let (verify_result_tx, verify_result_rx) = mpsc::channel(1);
5228 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
5229 let (piece_ready_tx, _piece_ready_rx) = broadcast::channel(1);
5230 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(num_pieces));
5231 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
5232 let (alert_tx, _alert_rx) = broadcast::channel(64);
5233 let (_disk_mgr_tx, _disk_mgr_rx) = mpsc::channel::<crate::disk::DiskJob>(1);
5234
5235 let stream_read_semaphore = Arc::new(tokio::sync::Semaphore::new(8));
5236 let alert_mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
5237
5238 let (disk_manager, _disk_join) =
5240 crate::disk::DiskManagerHandle::new(crate::disk::DiskConfig::default());
5241
5242 let ban_manager = Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
5243 crate::ban::BanConfig::default(),
5244 )));
5245 let ip_filter = Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
5246
5247 let upload_bucket = crate::rate_limiter::TokenBucket::new(0);
5248 let download_bucket = Arc::new(parking_lot::Mutex::new(
5249 crate::rate_limiter::TokenBucket::new(0),
5250 ));
5251 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(0, 0, 0, 0, 0, 0);
5252
5253 let dht_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5254 let dht_v6_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5255 let factory = Arc::new(crate::transport::NetworkFactory::tokio());
5256
5257 let we_have = Bitfield::new(num_pieces);
5261 let mut wanted = Bitfield::new(num_pieces);
5262 for i in 0..num_pieces {
5263 wanted.set(i);
5264 }
5265 let atomic_states = Arc::new(crate::piece_reservation::AtomicPieceStates::new(
5266 num_pieces, &we_have, &wanted,
5267 ));
5268
5269 let (order_map_tx, _order_map_rx_seed) =
5270 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
5271
5272 Self {
5273 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(0),
5274 config,
5275 info_hash,
5276 our_peer_id,
5277 state: TorrentState::Downloading,
5278 disk: None,
5279 disk_manager,
5280 chunk_tracker: None,
5281 lengths: None,
5282 num_pieces,
5283 file_priorities: Vec::new(),
5284 wanted_pieces: Bitfield::new(num_pieces),
5285 end_game: EndGame::new(),
5286 streaming_pieces: BTreeSet::new(),
5287 time_critical_pieces: BTreeSet::new(),
5288 streaming_cursors: Vec::new(),
5289 piece_ready_tx,
5290 have_watch_tx,
5291 have_watch_rx,
5292 stream_read_semaphore,
5293 peers: HashMap::new(),
5294 unchoke_durations: HashMap::new(),
5295 cached_peer_rates: FxHashMap::default(),
5296 refill_notify: Arc::new(tokio::sync::Notify::new()),
5297 atomic_states: Some(atomic_states),
5298 block_maps: None,
5299 steal_candidates: None,
5300 last_steal_populate: Instant::now(),
5301 piece_write_guards: None,
5302 soft_reap_buf: Vec::new(),
5303 eviction_history: std::collections::VecDeque::new(),
5304 force_immediate_choker_tick: false,
5305 piece_tracker: None,
5306 order_map_tx,
5307 piece_owner: vec![None; num_pieces as usize],
5308 peer_slab: crate::piece_reservation::PeerSlab::new(),
5309 priority_pieces: BTreeSet::new(),
5310 max_in_flight: 512,
5311 reservation_notify: None,
5312 last_tick_dispatch_state: None,
5313 choker: Choker::new(4),
5314 user_seed_mode: false,
5315 user_forced: false,
5316 max_connections: 0,
5317 peer_states: None,
5318 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
5319 connect_permits: HashMap::new(),
5320 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
5321 std::collections::HashMap::new(),
5322 )),
5323 connect_rx: None,
5324 metadata_downloader: None,
5325 meta: None,
5326 cached_files: None,
5327 downloaded: 0,
5328 uploaded: 0,
5329 checking_progress: 0.0,
5330 total_download: 0,
5331 total_upload: 0,
5332 total_failed_bytes: 0,
5333 total_redundant_bytes: 0,
5334 added_time: 0,
5335 completed_time: 0,
5336 last_download: 0,
5337 last_upload: 0,
5338 last_seen_complete: 0,
5339 active_duration: 0,
5340 finished_duration: 0,
5341 seeding_duration: 0,
5342 active_since: None,
5343 state_duration_since: None,
5344 started_at: Instant::now(),
5345 moving_storage: false,
5346 has_incoming: false,
5347 need_save_resume: false,
5348 error: String::new(),
5349 error_file: -1,
5350 cmd_rx,
5351 event_tx,
5352 event_rx,
5353 write_error_rx,
5354 write_error_tx,
5355 verify_result_rx,
5356 verify_result_tx,
5357 pending_verify: HashSet::new(),
5358 piece_generations: vec![0u64; num_pieces as usize],
5359 hash_result_rx,
5360 hash_result_tx,
5361 listener: None,
5362 utp_socket: None,
5363 utp_socket_v6: None,
5364 tracker_manager: TrackerManager::empty(info_hash, our_peer_id, 0, 0, false),
5365 tracker_result_rx: None,
5366 dht_rx,
5367 dht_v6_rx,
5368 dht_enabled: false,
5369 dht_peers_rx: None,
5370 dht_v6_peers_rx: None,
5371 dht_v6_empty_count: 0,
5372 dht_v6_last_retry: None,
5373 alert_tx,
5374 alert_mask,
5375 upload_bucket,
5376 download_bucket,
5377 global_upload_bucket: None,
5378 global_download_bucket: None,
5379 slot_tuner: crate::slot_tuner::SlotTuner::disabled(4),
5380 upload_bytes_interval: 0,
5381 peak_download_rate: 0,
5382 web_seeds: HashMap::new(),
5383 banned_web_seeds: HashSet::new(),
5384 web_seed_in_flight: HashMap::new(),
5385 web_seed_stats: HashMap::new(),
5386 pex_peer_count: 0,
5387 lsd_peer_count: 0,
5388 super_seed: None,
5389 have_broadcast_tx,
5390 suggested_to_peers: HashMap::new(),
5391 predictive_have_sent: HashSet::new(),
5392 ban_manager,
5393 piece_contributors: HashMap::new(),
5394 parole_pieces: HashMap::new(),
5395 ip_filter,
5396 external_ip: None,
5397 share_lru: std::collections::VecDeque::new(),
5398 share_max_pieces: 0,
5399 plugins: Arc::new(Vec::new()),
5400 hash_picker: None,
5401 version: irontide_core::TorrentVersion::V1Only,
5402 meta_v2: None,
5403 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
5404 dht_v2_peers_rx: None,
5405 dht_v6_v2_peers_rx: None,
5406 magnet_selected_files: None,
5407 sam_session: None,
5408 i2p_accept_rx: None,
5409 i2p_peer_counter: 0,
5410 i2p_destinations: HashMap::new(),
5411 ssl_manager: None,
5412 rate_limiter_set,
5413 auto_sequential_active: false,
5414 factory,
5415 hash_pool_ref: None,
5416 connect_attempts: 0,
5417 connect_failures: 0,
5418 choke_rotations: 0,
5419 inflight_started: Vec::new(),
5420 completed_piece_times: std::collections::VecDeque::new(),
5421 piece_steals: 0,
5422 holepunch_relayed: 0,
5423 holepunch_relay_rate: HashMap::new(),
5424 holepunch_cooldowns: HashMap::new(),
5425 holepunch_pending: Vec::new(),
5426 counters: Arc::new(crate::stats::SessionCounters::new()),
5427 }
5428 }
5429}
5430
5431#[cfg(test)]
5436mod tests {
5437 use super::*;
5438 use bytes::Bytes;
5439 use futures::{SinkExt, StreamExt};
5440 use irontide_wire::{ExtHandshake, Handshake, Message, MessageCodec};
5441 use std::time::Duration;
5442 use tokio::io::{AsyncReadExt, AsyncWriteExt};
5443 use tokio::net::TcpListener;
5444 use tokio_util::codec::{FramedRead, FramedWrite};
5445
5446 #[test]
5449 fn initial_unchoke_slots_unlimited_returns_default_four() {
5450 assert_eq!(initial_unchoke_slots(-1), 4);
5451 }
5452
5453 #[test]
5454 fn initial_unchoke_slots_capped_returns_value() {
5455 assert_eq!(initial_unchoke_slots(1), 1);
5456 assert_eq!(initial_unchoke_slots(4), 4);
5457 assert_eq!(initial_unchoke_slots(16), 16);
5458 }
5459
5460 fn make_test_torrent(data: &[u8], piece_length: u64) -> TorrentMetaV1 {
5464 use serde::Serialize;
5465
5466 #[derive(Serialize)]
5467 struct Info<'a> {
5468 length: u64,
5469 name: &'a str,
5470 #[serde(rename = "piece length")]
5471 piece_length: u64,
5472 #[serde(with = "serde_bytes")]
5473 pieces: &'a [u8],
5474 }
5475
5476 #[derive(Serialize)]
5477 struct Torrent<'a> {
5478 info: Info<'a>,
5479 }
5480
5481 let mut pieces = Vec::new();
5482 let mut offset = 0;
5483 while offset < data.len() {
5484 let end = (offset + piece_length as usize).min(data.len());
5485 let hash = irontide_core::sha1(&data[offset..end]);
5486 pieces.extend_from_slice(hash.as_bytes());
5487 offset = end;
5488 }
5489
5490 let t = Torrent {
5491 info: Info {
5492 length: data.len() as u64,
5493 name: "test",
5494 piece_length,
5495 pieces: &pieces,
5496 },
5497 };
5498
5499 let bytes = irontide_bencode::to_bytes(&t).unwrap();
5500 torrent_from_bytes(&bytes).unwrap()
5501 }
5502
5503 fn test_config() -> TorrentConfig {
5504 TorrentConfig {
5505 listen_port: 0, max_peers: 200,
5507 target_request_queue: 5,
5508 download_dir: std::path::PathBuf::from("/tmp"),
5509 enable_dht: false,
5510 enable_pex: false,
5511 enable_fast: false,
5512 seed_ratio_limit: None,
5513 seed_time_limit_secs: None,
5514 inactive_seed_time_limit_secs: None,
5515 strict_end_game: true,
5516 upload_rate_limit: 0,
5517 download_rate_limit: 0,
5518 max_uploads_per_torrent: -1,
5519 encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
5520 enable_utp: false,
5521 enable_web_seed: true,
5522 enable_holepunch: false,
5523 enable_bep40_eviction: true,
5524 max_web_seeds: 4,
5525 web_seed_retry_base_secs: 10,
5526 web_seed_retry_factor: 6,
5527 web_seed_retry_cap_secs: 3600,
5528 web_seed_max_failures: 10,
5529 super_seeding: false,
5530 upload_only_announce: true,
5531 hashing_threads: 2,
5532 sequential_download: false,
5533 initial_picker_threshold: 4,
5534 whole_pieces_threshold: 20,
5535 snub_timeout_secs: 15,
5536 readahead_pieces: 8,
5537 streaming_timeout_escalation: true,
5538 max_concurrent_stream_reads: 8,
5539 proxy: crate::proxy::ProxyConfig::default(),
5540 anonymous_mode: false,
5541 share_mode: false,
5542 enable_i2p: false,
5543 allow_i2p_mixed: false,
5544 ssl_listen_port: 0,
5545 seed_choking_algorithm: crate::choker::SeedChokingAlgorithm::FastestUpload,
5546 choking_algorithm: crate::choker::ChokingAlgorithm::FixedSlots,
5547 piece_extent_affinity: true,
5548 suggest_mode: false,
5549 max_suggest_pieces: 10,
5550 predictive_piece_announce_ms: 0,
5551 mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
5552 auto_sequential: true,
5553 storage_mode: irontide_core::StorageMode::Auto,
5554 preallocate_mode: None,
5555 block_request_timeout_secs: 60,
5556 enable_lsd: false,
5557 force_proxy: false,
5558 steal_threshold_ratio: 10.0,
5559 steal_threshold_endgame: 3.0,
5560 peer_read_timeout_secs: 0, peer_write_timeout_secs: 0, data_contribution_timeout_secs: 0, pass0_grace_secs: 60,
5565 proactive_evictions_per_minute_limit: 30,
5566 eviction_ban_duration_secs: 600,
5567 eviction_ban_set_cap: 1024,
5568 choke_rotation_max_evictions: 0, max_concurrent_connects: 128,
5570 connect_soft_timeout: 3,
5571 dispatch_backlog_cap: 8,
5572 event_backlog_cap: 32,
5573 use_actor_dispatch: true,
5574 web_seed_progress_throttle_ms: 250,
5575 url_security: crate::url_guard::UrlSecurityConfig::default(),
5576 peer_connect_timeout: 2,
5577 peer_dscp: 0x08,
5578 initial_queue_depth: 128,
5579 max_request_queue_depth: 250,
5580 request_queue_time: 3.0,
5581 max_metadata_size: 4 * 1024 * 1024,
5582 max_message_size: 16 * 1024 * 1024,
5583 max_piece_length: 32 * 1024 * 1024,
5584 max_outstanding_requests: 500,
5585 max_in_flight_pieces: 20,
5586 use_block_stealing: true,
5587 steal_stale_piece_secs: 2,
5588 fixed_pipeline_depth: 128,
5589 lock_warn_threshold_ms: 0, filesystem_direct_io: false,
5591 category: None,
5592 tags: Vec::new(),
5593 }
5594 }
5595
5596 fn make_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
5597 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
5598 Arc::new(MemoryStorage::new(lengths))
5599 }
5600
5601 fn make_seeded_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
5602 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
5603 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
5604 let num_pieces = lengths.num_pieces();
5606 for p in 0..num_pieces {
5607 let piece_size = lengths.piece_size(p) as usize;
5608 let offset = lengths.piece_offset(p) as usize;
5609 let end = offset + piece_size;
5610 storage.write_chunk(p, 0, &data[offset..end]).unwrap();
5611 }
5612 storage
5613 }
5614
5615 fn test_alert_channel() -> (broadcast::Sender<Alert>, Arc<AtomicU32>) {
5616 let (tx, _) = broadcast::channel(64);
5617 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
5618 (tx, mask)
5619 }
5620
5621 fn test_ban_manager() -> crate::session::SharedBanManager {
5622 Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
5623 crate::ban::BanConfig::default(),
5624 )))
5625 }
5626
5627 fn test_ip_filter() -> crate::session::SharedIpFilter {
5628 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()))
5629 }
5630
5631 fn test_disk_manager() -> (DiskManagerHandle, tokio::task::JoinHandle<()>) {
5632 DiskManagerHandle::new(crate::disk::DiskConfig::default())
5633 }
5634
5635 async fn test_register_disk(
5636 info_hash: Id20,
5637 storage: Arc<dyn TorrentStorage>,
5638 ) -> (DiskHandle, DiskManagerHandle, tokio::task::JoinHandle<()>) {
5639 let (dm, join) = test_disk_manager();
5640 let dh = dm.register_torrent(info_hash, storage).await;
5641 (dh, dm, join)
5642 }
5643
5644 fn test_dht_rx() -> irontide_dht::DhtReceiver {
5647 let bx = irontide_dht::DhtBroadcast::new(None);
5650 bx.subscribe()
5651 }
5652
5653 const HANDSHAKE_SIZE: usize = 68;
5655
5656 #[tokio::test]
5659 async fn create_from_torrent() {
5660 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
5663 let config = test_config();
5664
5665 let (atx, amask) = test_alert_channel();
5666 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5667 let handle = TorrentHandle::from_torrent(
5668 meta,
5669 irontide_core::TorrentVersion::V1Only,
5670 None,
5671 dh,
5672 dm,
5673 config,
5674 test_dht_rx(),
5675 test_dht_rx(),
5676 None,
5677 None,
5678 crate::slot_tuner::SlotTuner::disabled(4),
5679 atx,
5680 amask,
5681 None,
5682 None,
5683 test_ban_manager(),
5684 test_ip_filter(),
5685 Arc::new(Vec::new()),
5686 None,
5687 None,
5688 Arc::new(crate::transport::NetworkFactory::tokio()),
5689 None, Arc::new(crate::stats::SessionCounters::new()),
5691 )
5692 .await
5693 .unwrap();
5694
5695 let stats = handle.stats().await.unwrap();
5696 assert_eq!(stats.state, TorrentState::Downloading);
5697 assert_eq!(stats.pieces_total, 2);
5698 assert_eq!(stats.pieces_have, 0);
5699 assert_eq!(stats.peers_connected, 0);
5700
5701 handle.shutdown().await.unwrap();
5702 }
5703
5704 #[tokio::test]
5707 async fn create_from_magnet() {
5708 let magnet = Magnet {
5709 info_hashes: irontide_core::InfoHashes::v1_only(
5710 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
5711 ),
5712 display_name: Some("test".into()),
5713 trackers: vec![],
5714 peers: vec![],
5715 selected_files: None,
5716 };
5717 let config = test_config();
5718
5719 let (atx, amask) = test_alert_channel();
5720 let (dm, _dj) = test_disk_manager();
5721 let handle = TorrentHandle::from_magnet(
5722 magnet,
5723 dm,
5724 config,
5725 test_dht_rx(),
5726 test_dht_rx(),
5727 None,
5728 None,
5729 crate::slot_tuner::SlotTuner::disabled(4),
5730 atx,
5731 amask,
5732 None,
5733 None,
5734 test_ban_manager(),
5735 test_ip_filter(),
5736 Arc::new(Vec::new()),
5737 None,
5738 None,
5739 Arc::new(crate::transport::NetworkFactory::tokio()),
5740 None, Arc::new(crate::stats::SessionCounters::new()),
5742 )
5743 .await
5744 .unwrap();
5745
5746 let stats = handle.stats().await.unwrap();
5747 assert_eq!(stats.state, TorrentState::FetchingMetadata);
5748 assert_eq!(stats.pieces_total, 0);
5749
5750 handle.shutdown().await.unwrap();
5751 }
5752
5753 #[tokio::test]
5756 async fn add_peers_increases_available() {
5757 let data = vec![0xAB; 32768];
5758 let meta = make_test_torrent(&data, 16384);
5759 let storage = make_storage(&data, 16384);
5760 let config = test_config();
5761
5762 let (atx, amask) = test_alert_channel();
5763 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5764 let handle = TorrentHandle::from_torrent(
5765 meta,
5766 irontide_core::TorrentVersion::V1Only,
5767 None,
5768 dh,
5769 dm,
5770 config,
5771 test_dht_rx(),
5772 test_dht_rx(),
5773 None,
5774 None,
5775 crate::slot_tuner::SlotTuner::disabled(4),
5776 atx,
5777 amask,
5778 None,
5779 None,
5780 test_ban_manager(),
5781 test_ip_filter(),
5782 Arc::new(Vec::new()),
5783 None,
5784 None,
5785 Arc::new(crate::transport::NetworkFactory::tokio()),
5786 None, Arc::new(crate::stats::SessionCounters::new()),
5788 )
5789 .await
5790 .unwrap();
5791
5792 let listener1 = TcpListener::bind("127.0.0.1:0").await.unwrap();
5794 let addr1 = listener1.local_addr().unwrap();
5795 let listener2 = TcpListener::bind("127.0.0.1:0").await.unwrap();
5796 let addr2 = listener2.local_addr().unwrap();
5797
5798 handle
5799 .add_peers(vec![addr1, addr2], PeerSource::Tracker)
5800 .await
5801 .unwrap();
5802
5803 tokio::time::sleep(Duration::from_millis(100)).await;
5805
5806 let stats = handle.stats().await.unwrap();
5807 assert!(
5809 stats.peers_available + stats.peers_connected >= 2,
5810 "expected at least 2 peers known, got available={}, connected={}",
5811 stats.peers_available,
5812 stats.peers_connected
5813 );
5814
5815 handle.shutdown().await.unwrap();
5816 }
5817
5818 #[tokio::test]
5821 async fn stats_reporting() {
5822 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
5825 let config = test_config();
5826
5827 let (atx, amask) = test_alert_channel();
5828 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5829 let handle = TorrentHandle::from_torrent(
5830 meta,
5831 irontide_core::TorrentVersion::V1Only,
5832 None,
5833 dh,
5834 dm,
5835 config,
5836 test_dht_rx(),
5837 test_dht_rx(),
5838 None,
5839 None,
5840 crate::slot_tuner::SlotTuner::disabled(4),
5841 atx,
5842 amask,
5843 None,
5844 None,
5845 test_ban_manager(),
5846 test_ip_filter(),
5847 Arc::new(Vec::new()),
5848 None,
5849 None,
5850 Arc::new(crate::transport::NetworkFactory::tokio()),
5851 None, Arc::new(crate::stats::SessionCounters::new()),
5853 )
5854 .await
5855 .unwrap();
5856
5857 let stats = handle.stats().await.unwrap();
5858 assert_eq!(stats.state, TorrentState::Downloading);
5859 assert_eq!(stats.downloaded, 0);
5860 assert_eq!(stats.uploaded, 0);
5861 assert_eq!(stats.pieces_have, 0);
5862 assert_eq!(stats.pieces_total, 4);
5863 assert_eq!(stats.peers_connected, 0);
5864 assert_eq!(stats.peers_available, 0);
5865
5866 handle.shutdown().await.unwrap();
5867 }
5868
5869 #[tokio::test]
5872 async fn private_torrent_disables_dht_pex() {
5873 use serde::Serialize;
5875
5876 #[derive(Serialize)]
5877 struct Info<'a> {
5878 length: u64,
5879 name: &'a str,
5880 #[serde(rename = "piece length")]
5881 piece_length: u64,
5882 #[serde(with = "serde_bytes")]
5883 pieces: &'a [u8],
5884 private: i64,
5885 }
5886
5887 #[derive(Serialize)]
5888 struct Torrent<'a> {
5889 info: Info<'a>,
5890 }
5891
5892 let data = vec![0xAB; 16384];
5893 let hash = irontide_core::sha1(&data);
5894 let mut pieces = Vec::new();
5895 pieces.extend_from_slice(hash.as_bytes());
5896
5897 let t = Torrent {
5898 info: Info {
5899 length: data.len() as u64,
5900 name: "private_test",
5901 piece_length: 16384,
5902 pieces: &pieces,
5903 private: 1,
5904 },
5905 };
5906
5907 let bytes = irontide_bencode::to_bytes(&t).unwrap();
5908 let meta = torrent_from_bytes(&bytes).unwrap();
5909 assert_eq!(meta.info.private, Some(1));
5910
5911 let storage = make_storage(&data, 16384);
5912 let mut config = test_config();
5913 config.enable_dht = true;
5914 config.enable_pex = true;
5915
5916 let (atx, amask) = test_alert_channel();
5918 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5919 let handle = TorrentHandle::from_torrent(
5920 meta,
5921 irontide_core::TorrentVersion::V1Only,
5922 None,
5923 dh,
5924 dm,
5925 config,
5926 test_dht_rx(),
5927 test_dht_rx(),
5928 None,
5929 None,
5930 crate::slot_tuner::SlotTuner::disabled(4),
5931 atx,
5932 amask,
5933 None,
5934 None,
5935 test_ban_manager(),
5936 test_ip_filter(),
5937 Arc::new(Vec::new()),
5938 None,
5939 None,
5940 Arc::new(crate::transport::NetworkFactory::tokio()),
5941 None, Arc::new(crate::stats::SessionCounters::new()),
5943 )
5944 .await
5945 .unwrap();
5946
5947 let stats = handle.stats().await.unwrap();
5951 assert_eq!(stats.state, TorrentState::Downloading);
5952
5953 handle.shutdown().await.unwrap();
5954 }
5955
5956 #[tokio::test]
5959 async fn shutdown_cleanup() {
5960 let data = vec![0xAB; 16384];
5961 let meta = make_test_torrent(&data, 16384);
5962 let storage = make_storage(&data, 16384);
5963 let config = test_config();
5964
5965 let (atx, amask) = test_alert_channel();
5966 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5967 let handle = TorrentHandle::from_torrent(
5968 meta,
5969 irontide_core::TorrentVersion::V1Only,
5970 None,
5971 dh,
5972 dm,
5973 config,
5974 test_dht_rx(),
5975 test_dht_rx(),
5976 None,
5977 None,
5978 crate::slot_tuner::SlotTuner::disabled(4),
5979 atx,
5980 amask,
5981 None,
5982 None,
5983 test_ban_manager(),
5984 test_ip_filter(),
5985 Arc::new(Vec::new()),
5986 None,
5987 None,
5988 Arc::new(crate::transport::NetworkFactory::tokio()),
5989 None, Arc::new(crate::stats::SessionCounters::new()),
5991 )
5992 .await
5993 .unwrap();
5994
5995 handle.shutdown().await.unwrap();
5996
5997 tokio::time::sleep(Duration::from_millis(50)).await;
5999 let result = handle.stats().await;
6000 assert!(result.is_err());
6001 }
6002
6003 #[tokio::test]
6006 async fn duplicate_peers_ignored() {
6007 let data = vec![0xAB; 16384];
6008 let meta = make_test_torrent(&data, 16384);
6009 let storage = make_storage(&data, 16384);
6010 let config = test_config();
6011
6012 let (atx, amask) = test_alert_channel();
6013 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6014 let handle = TorrentHandle::from_torrent(
6015 meta,
6016 irontide_core::TorrentVersion::V1Only,
6017 None,
6018 dh,
6019 dm,
6020 config,
6021 test_dht_rx(),
6022 test_dht_rx(),
6023 None,
6024 None,
6025 crate::slot_tuner::SlotTuner::disabled(4),
6026 atx,
6027 amask,
6028 None,
6029 None,
6030 test_ban_manager(),
6031 test_ip_filter(),
6032 Arc::new(Vec::new()),
6033 None,
6034 None,
6035 Arc::new(crate::transport::NetworkFactory::tokio()),
6036 None, Arc::new(crate::stats::SessionCounters::new()),
6038 )
6039 .await
6040 .unwrap();
6041
6042 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6044 let addr = listener.local_addr().unwrap();
6045 handle
6046 .add_peers(vec![addr, addr, addr], PeerSource::Tracker)
6047 .await
6048 .unwrap();
6049
6050 tokio::time::sleep(Duration::from_millis(100)).await;
6051 let stats = handle.stats().await.unwrap();
6052 assert!(
6054 stats.peers_available + stats.peers_connected <= 1,
6055 "expected at most 1 unique peer, got available={}, connected={}",
6056 stats.peers_available,
6057 stats.peers_connected
6058 );
6059
6060 handle.shutdown().await.unwrap();
6061 }
6062
6063 #[tokio::test]
6066 async fn cloned_handle_shares_actor() {
6067 let data = vec![0xAB; 16384];
6068 let meta = make_test_torrent(&data, 16384);
6069 let storage = make_storage(&data, 16384);
6070 let config = test_config();
6071
6072 let (atx, amask) = test_alert_channel();
6073 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6074 let handle = TorrentHandle::from_torrent(
6075 meta,
6076 irontide_core::TorrentVersion::V1Only,
6077 None,
6078 dh,
6079 dm,
6080 config,
6081 test_dht_rx(),
6082 test_dht_rx(),
6083 None,
6084 None,
6085 crate::slot_tuner::SlotTuner::disabled(4),
6086 atx,
6087 amask,
6088 None,
6089 None,
6090 test_ban_manager(),
6091 test_ip_filter(),
6092 Arc::new(Vec::new()),
6093 None,
6094 None,
6095 Arc::new(crate::transport::NetworkFactory::tokio()),
6096 None, Arc::new(crate::stats::SessionCounters::new()),
6098 )
6099 .await
6100 .unwrap();
6101 let handle2 = handle.clone();
6102
6103 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6105 let peer_addr = listener.local_addr().unwrap();
6106
6107 handle
6109 .add_peers(vec![peer_addr], PeerSource::Tracker)
6110 .await
6111 .unwrap();
6112
6113 tokio::time::sleep(Duration::from_millis(100)).await;
6114
6115 let stats = handle2.stats().await.unwrap();
6117 assert!(
6118 stats.peers_available + stats.peers_connected >= 1,
6119 "expected at least 1 peer known, got available={}, connected={}",
6120 stats.peers_available,
6121 stats.peers_connected
6122 );
6123
6124 handle.shutdown().await.unwrap();
6125 }
6126
6127 #[tokio::test]
6130 async fn peer_connect_and_disconnect_via_listener() {
6131 let data = vec![0xAB; 16384];
6132 let meta = make_test_torrent(&data, 16384);
6133 let info_hash = meta.info_hash;
6134 let storage = make_storage(&data, 16384);
6135
6136 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6138 let listen_addr = listener.local_addr().unwrap();
6139
6140 let config = TorrentConfig {
6141 listen_port: listen_addr.port(),
6142 ..test_config()
6143 };
6144
6145 drop(listener);
6147
6148 let (atx, amask) = test_alert_channel();
6149 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6150 let handle = TorrentHandle::from_torrent(
6151 meta,
6152 irontide_core::TorrentVersion::V1Only,
6153 None,
6154 dh,
6155 dm,
6156 config,
6157 test_dht_rx(),
6158 test_dht_rx(),
6159 None,
6160 None,
6161 crate::slot_tuner::SlotTuner::disabled(4),
6162 atx,
6163 amask,
6164 None,
6165 None,
6166 test_ban_manager(),
6167 test_ip_filter(),
6168 Arc::new(Vec::new()),
6169 None,
6170 None,
6171 Arc::new(crate::transport::NetworkFactory::tokio()),
6172 None, Arc::new(crate::stats::SessionCounters::new()),
6174 )
6175 .await
6176 .unwrap();
6177
6178 tokio::time::sleep(Duration::from_millis(50)).await;
6180
6181 let mut stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6183
6184 let remote_id = Id20::from_hex("1111111111111111111111111111111111111111").unwrap();
6186 let remote_hs = Handshake::new(info_hash, remote_id);
6187 stream.write_all(&remote_hs.to_bytes()).await.unwrap();
6188 stream.flush().await.unwrap();
6189
6190 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6191 stream.read_exact(&mut hs_buf).await.unwrap();
6192 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
6193 assert_eq!(their_hs.info_hash, info_hash);
6194
6195 tokio::time::sleep(Duration::from_millis(100)).await;
6197
6198 let stats = handle.stats().await.unwrap();
6199 assert_eq!(stats.peers_connected, 1);
6200
6201 drop(stream);
6203
6204 tokio::time::sleep(Duration::from_millis(200)).await;
6206
6207 let stats = handle.stats().await.unwrap();
6208 assert_eq!(stats.peers_connected, 0);
6209
6210 handle.shutdown().await.unwrap();
6211 }
6212
6213 #[tokio::test]
6219 async fn piece_download_and_verify() {
6220 let data = vec![0xCDu8; 16384];
6222 let meta = make_test_torrent(&data, 16384);
6223 let info_hash = meta.info_hash;
6224 let storage = make_storage(&data, 16384);
6225
6226 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6227 let listen_addr = listener.local_addr().unwrap();
6228 drop(listener);
6229
6230 let config = TorrentConfig {
6231 listen_port: listen_addr.port(),
6232 ..test_config()
6233 };
6234
6235 let (atx, amask) = test_alert_channel();
6236 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6237 let handle = TorrentHandle::from_torrent(
6238 meta,
6239 irontide_core::TorrentVersion::V1Only,
6240 None,
6241 dh,
6242 dm,
6243 config,
6244 test_dht_rx(),
6245 test_dht_rx(),
6246 None,
6247 None,
6248 crate::slot_tuner::SlotTuner::disabled(4),
6249 atx,
6250 amask,
6251 None,
6252 None,
6253 test_ban_manager(),
6254 test_ip_filter(),
6255 Arc::new(Vec::new()),
6256 None,
6257 None,
6258 Arc::new(crate::transport::NetworkFactory::tokio()),
6259 None, Arc::new(crate::stats::SessionCounters::new()),
6261 )
6262 .await
6263 .unwrap();
6264
6265 tokio::time::sleep(Duration::from_millis(50)).await;
6266
6267 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6269 let remote_id = Id20::from_hex("2222222222222222222222222222222222222222").unwrap();
6270
6271 let mock_data = data.clone();
6273 let mock_task = tokio::spawn(async move {
6274 let (reader, writer) = tokio::io::split(stream);
6275 let mut reader = reader;
6276 let mut writer = writer;
6277
6278 let hs = Handshake::new(info_hash, remote_id);
6280 writer.write_all(&hs.to_bytes()).await.unwrap();
6281 writer.flush().await.unwrap();
6282
6283 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6284 reader.read_exact(&mut hs_buf).await.unwrap();
6285
6286 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6288 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6289
6290 let _msg = framed_read.next().await;
6292
6293 let ext_hs = ExtHandshake::new();
6295 let payload = ext_hs.to_bytes().unwrap();
6296 framed_write
6297 .send(Message::Extended { ext_id: 0, payload })
6298 .await
6299 .unwrap();
6300
6301 let mut bf = Bitfield::new(1);
6303 bf.set(0);
6304 framed_write
6305 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6306 .await
6307 .unwrap();
6308
6309 framed_write.send(Message::Unchoke).await.unwrap();
6311
6312 while let Some(Ok(msg)) = framed_read.next().await {
6314 if let Message::Request {
6315 index,
6316 begin,
6317 length,
6318 } = msg
6319 {
6320 let start = begin as usize;
6321 let end = start + length as usize;
6322 let piece_data = &mock_data[start..end];
6323 framed_write
6324 .send(Message::Piece {
6325 index,
6326 begin,
6327 data_0: Bytes::copy_from_slice(piece_data),
6328 data_1: Bytes::new(),
6329 })
6330 .await
6331 .unwrap();
6332 }
6333 }
6334 });
6335
6336 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6338 loop {
6339 tokio::time::sleep(Duration::from_millis(100)).await;
6340 let stats = handle.stats().await.unwrap();
6341 if stats.state == TorrentState::Seeding {
6342 assert_eq!(stats.pieces_have, 1);
6343 assert_eq!(stats.pieces_total, 1);
6344 break;
6345 }
6346 if tokio::time::Instant::now() > deadline {
6347 let stats = handle.stats().await.unwrap();
6348 panic!(
6349 "download did not complete within 5s, state={:?}, have={}/{}",
6350 stats.state, stats.pieces_have, stats.pieces_total
6351 );
6352 }
6353 }
6354
6355 handle.shutdown().await.unwrap();
6356 mock_task.abort();
6357 }
6358
6359 #[tokio::test]
6362 async fn failed_piece_verification() {
6363 let data = vec![0xEEu8; 16384];
6365 let meta = make_test_torrent(&data, 16384);
6366 let info_hash = meta.info_hash;
6367 let storage = make_storage(&data, 16384);
6368
6369 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6370 let listen_addr = listener.local_addr().unwrap();
6371 drop(listener);
6372
6373 let config = TorrentConfig {
6374 listen_port: listen_addr.port(),
6375 ..test_config()
6376 };
6377
6378 let (atx, amask) = test_alert_channel();
6379 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6380 let handle = TorrentHandle::from_torrent(
6381 meta,
6382 irontide_core::TorrentVersion::V1Only,
6383 None,
6384 dh,
6385 dm,
6386 config,
6387 test_dht_rx(),
6388 test_dht_rx(),
6389 None,
6390 None,
6391 crate::slot_tuner::SlotTuner::disabled(4),
6392 atx,
6393 amask,
6394 None,
6395 None,
6396 test_ban_manager(),
6397 test_ip_filter(),
6398 Arc::new(Vec::new()),
6399 None,
6400 None,
6401 Arc::new(crate::transport::NetworkFactory::tokio()),
6402 None, Arc::new(crate::stats::SessionCounters::new()),
6404 )
6405 .await
6406 .unwrap();
6407
6408 tokio::time::sleep(Duration::from_millis(50)).await;
6409
6410 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6412 let remote_id = Id20::from_hex("3333333333333333333333333333333333333333").unwrap();
6413
6414 let correct_data = data.clone();
6415 let mock_task = tokio::spawn(async move {
6416 let (reader, writer) = tokio::io::split(stream);
6417
6418 let mut writer = writer;
6420 let mut reader = reader;
6421 let hs = Handshake::new(info_hash, remote_id);
6422 writer.write_all(&hs.to_bytes()).await.unwrap();
6423 writer.flush().await.unwrap();
6424
6425 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6426 reader.read_exact(&mut hs_buf).await.unwrap();
6427
6428 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6429 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6430
6431 let _msg = framed_read.next().await;
6433
6434 let ext_hs = ExtHandshake::new();
6436 let payload = ext_hs.to_bytes().unwrap();
6437 framed_write
6438 .send(Message::Extended { ext_id: 0, payload })
6439 .await
6440 .unwrap();
6441
6442 let mut bf = Bitfield::new(1);
6444 bf.set(0);
6445 framed_write
6446 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6447 .await
6448 .unwrap();
6449
6450 framed_write.send(Message::Unchoke).await.unwrap();
6452
6453 let mut request_count = 0u32;
6454 while let Some(Ok(msg)) = framed_read.next().await {
6455 if let Message::Request {
6456 index,
6457 begin,
6458 length,
6459 } = msg
6460 {
6461 request_count += 1;
6462 let piece_data = if request_count <= 1 {
6463 vec![0xFF; length as usize]
6465 } else {
6466 let start = begin as usize;
6468 let end = start + length as usize;
6469 correct_data[start..end].to_vec()
6470 };
6471 framed_write
6472 .send(Message::Piece {
6473 index,
6474 begin,
6475 data_0: Bytes::from(piece_data),
6476 data_1: Bytes::new(),
6477 })
6478 .await
6479 .unwrap();
6480 }
6481 }
6482 });
6483
6484 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6486 loop {
6487 tokio::time::sleep(Duration::from_millis(100)).await;
6488 let stats = handle.stats().await.unwrap();
6489 if stats.state == TorrentState::Seeding {
6490 assert_eq!(stats.pieces_have, 1);
6491 break;
6492 }
6493 if tokio::time::Instant::now() > deadline {
6494 let stats = handle.stats().await.unwrap();
6495 panic!(
6496 "download did not complete after retry within 5s, state={:?}, have={}",
6497 stats.state, stats.pieces_have,
6498 );
6499 }
6500 }
6501
6502 handle.shutdown().await.unwrap();
6503 mock_task.abort();
6504 }
6505
6506 #[tokio::test]
6509 async fn complete_transitions_state() {
6510 let data = vec![0xBBu8; 32768];
6512 let meta = make_test_torrent(&data, 16384);
6513 let info_hash = meta.info_hash;
6514 let storage = make_storage(&data, 16384);
6515
6516 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6517 let listen_addr = listener.local_addr().unwrap();
6518 drop(listener);
6519
6520 let config = TorrentConfig {
6521 listen_port: listen_addr.port(),
6522 ..test_config()
6523 };
6524
6525 let (atx, amask) = test_alert_channel();
6526 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6527 let handle = TorrentHandle::from_torrent(
6528 meta,
6529 irontide_core::TorrentVersion::V1Only,
6530 None,
6531 dh,
6532 dm,
6533 config,
6534 test_dht_rx(),
6535 test_dht_rx(),
6536 None,
6537 None,
6538 crate::slot_tuner::SlotTuner::disabled(4),
6539 atx,
6540 amask,
6541 None,
6542 None,
6543 test_ban_manager(),
6544 test_ip_filter(),
6545 Arc::new(Vec::new()),
6546 None,
6547 None,
6548 Arc::new(crate::transport::NetworkFactory::tokio()),
6549 None, Arc::new(crate::stats::SessionCounters::new()),
6551 )
6552 .await
6553 .unwrap();
6554
6555 tokio::time::sleep(Duration::from_millis(50)).await;
6556
6557 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6559 let remote_id = Id20::from_hex("4444444444444444444444444444444444444444").unwrap();
6560
6561 let mock_data = data.clone();
6562 let mock_task = tokio::spawn(async move {
6563 let (reader, writer) = tokio::io::split(stream);
6564 let mut writer = writer;
6565 let mut reader = reader;
6566
6567 let hs = Handshake::new(info_hash, remote_id);
6568 writer.write_all(&hs.to_bytes()).await.unwrap();
6569 writer.flush().await.unwrap();
6570
6571 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6572 reader.read_exact(&mut hs_buf).await.unwrap();
6573
6574 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6575 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6576
6577 let _msg = framed_read.next().await;
6579
6580 let ext_hs = ExtHandshake::new();
6582 let payload = ext_hs.to_bytes().unwrap();
6583 framed_write
6584 .send(Message::Extended { ext_id: 0, payload })
6585 .await
6586 .unwrap();
6587
6588 let mut bf = Bitfield::new(2);
6590 bf.set(0);
6591 bf.set(1);
6592 framed_write
6593 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6594 .await
6595 .unwrap();
6596
6597 framed_write.send(Message::Unchoke).await.unwrap();
6598
6599 while let Some(Ok(msg)) = framed_read.next().await {
6600 if let Message::Request {
6601 index,
6602 begin,
6603 length,
6604 } = msg
6605 {
6606 let abs_start = (index as usize * 16384) + begin as usize;
6607 let abs_end = abs_start + length as usize;
6608 let piece_data = &mock_data[abs_start..abs_end];
6609 framed_write
6610 .send(Message::Piece {
6611 index,
6612 begin,
6613 data_0: Bytes::copy_from_slice(piece_data),
6614 data_1: Bytes::new(),
6615 })
6616 .await
6617 .unwrap();
6618 }
6619 }
6620 });
6621
6622 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6623 loop {
6624 tokio::time::sleep(Duration::from_millis(100)).await;
6625 let stats = handle.stats().await.unwrap();
6626 if stats.state == TorrentState::Seeding {
6627 assert_eq!(stats.pieces_have, 2);
6628 assert_eq!(stats.pieces_total, 2);
6629 break;
6630 }
6631 if tokio::time::Instant::now() > deadline {
6632 let stats = handle.stats().await.unwrap();
6633 panic!(
6634 "expected Complete, got {:?}, have={}/{}",
6635 stats.state, stats.pieces_have, stats.pieces_total
6636 );
6637 }
6638 }
6639
6640 handle.shutdown().await.unwrap();
6641 mock_task.abort();
6642 }
6643
6644 #[tokio::test]
6647 async fn multi_chunk_piece_download() {
6648 let data = vec![0xAAu8; 32768];
6650 let meta = make_test_torrent(&data, 32768);
6651 let info_hash = meta.info_hash;
6652 let storage = make_storage(&data, 32768);
6653
6654 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6655 let listen_addr = listener.local_addr().unwrap();
6656 drop(listener);
6657
6658 let config = TorrentConfig {
6659 listen_port: listen_addr.port(),
6660 ..test_config()
6661 };
6662
6663 let (atx, amask) = test_alert_channel();
6664 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6665 let handle = TorrentHandle::from_torrent(
6666 meta,
6667 irontide_core::TorrentVersion::V1Only,
6668 None,
6669 dh,
6670 dm,
6671 config,
6672 test_dht_rx(),
6673 test_dht_rx(),
6674 None,
6675 None,
6676 crate::slot_tuner::SlotTuner::disabled(4),
6677 atx,
6678 amask,
6679 None,
6680 None,
6681 test_ban_manager(),
6682 test_ip_filter(),
6683 Arc::new(Vec::new()),
6684 None,
6685 None,
6686 Arc::new(crate::transport::NetworkFactory::tokio()),
6687 None, Arc::new(crate::stats::SessionCounters::new()),
6689 )
6690 .await
6691 .unwrap();
6692
6693 tokio::time::sleep(Duration::from_millis(50)).await;
6694
6695 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6696 let remote_id = Id20::from_hex("5555555555555555555555555555555555555555").unwrap();
6697
6698 let mock_data = data.clone();
6699 let mock_task = tokio::spawn(async move {
6700 let (reader, writer) = tokio::io::split(stream);
6701 let mut writer = writer;
6702 let mut reader = reader;
6703
6704 let hs = Handshake::new(info_hash, remote_id);
6705 writer.write_all(&hs.to_bytes()).await.unwrap();
6706 writer.flush().await.unwrap();
6707
6708 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6709 reader.read_exact(&mut hs_buf).await.unwrap();
6710
6711 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6712 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6713
6714 let _msg = framed_read.next().await;
6715
6716 let ext_hs = ExtHandshake::new();
6717 let payload = ext_hs.to_bytes().unwrap();
6718 framed_write
6719 .send(Message::Extended { ext_id: 0, payload })
6720 .await
6721 .unwrap();
6722
6723 let mut bf = Bitfield::new(1);
6724 bf.set(0);
6725 framed_write
6726 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6727 .await
6728 .unwrap();
6729
6730 framed_write.send(Message::Unchoke).await.unwrap();
6731
6732 while let Some(Ok(msg)) = framed_read.next().await {
6733 if let Message::Request {
6734 index: _,
6735 begin,
6736 length,
6737 } = msg
6738 {
6739 let start = begin as usize;
6740 let end = start + length as usize;
6741 framed_write
6742 .send(Message::Piece {
6743 index: 0,
6744 begin,
6745 data_0: Bytes::copy_from_slice(&mock_data[start..end]),
6746 data_1: Bytes::new(),
6747 })
6748 .await
6749 .unwrap();
6750 }
6751 }
6752 });
6753
6754 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6755 loop {
6756 tokio::time::sleep(Duration::from_millis(100)).await;
6757 let stats = handle.stats().await.unwrap();
6758 if stats.state == TorrentState::Seeding {
6759 assert_eq!(stats.pieces_have, 1);
6760 break;
6761 }
6762 assert!(
6763 tokio::time::Instant::now() <= deadline,
6764 "multi-chunk download did not complete within 5s"
6765 );
6766 }
6767
6768 handle.shutdown().await.unwrap();
6769 mock_task.abort();
6770 }
6771
6772 #[tokio::test]
6775 async fn seeder_leecher_integration() {
6776 let data = vec![0xDDu8; 32768]; let piece_length = 16384u64;
6779 let meta = make_test_torrent(&data, piece_length);
6780 let info_hash = meta.info_hash;
6781
6782 let seeder_storage = make_seeded_storage(&data, piece_length);
6784
6785 let seeder_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6791 let seeder_addr = seeder_listener.local_addr().unwrap();
6792
6793 let seeder_task = tokio::spawn(async move {
6794 let (stream, _addr) = seeder_listener.accept().await.unwrap();
6795 let (reader, writer) = tokio::io::split(stream);
6796 let mut writer = writer;
6797 let mut reader = reader;
6798
6799 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6801 reader.read_exact(&mut hs_buf).await.unwrap();
6802 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
6803 assert_eq!(their_hs.info_hash, info_hash);
6804
6805 let hs = Handshake::new(info_hash, PeerId::generate().0);
6806 writer.write_all(&hs.to_bytes()).await.unwrap();
6807 writer.flush().await.unwrap();
6808
6809 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6810 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6811
6812 let _msg = framed_read.next().await;
6814
6815 let ext_hs = ExtHandshake::new();
6817 let payload = ext_hs.to_bytes().unwrap();
6818 framed_write
6819 .send(Message::Extended { ext_id: 0, payload })
6820 .await
6821 .unwrap();
6822
6823 let mut bf = Bitfield::new(2);
6825 bf.set(0);
6826 bf.set(1);
6827 framed_write
6828 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6829 .await
6830 .unwrap();
6831
6832 framed_write.send(Message::Unchoke).await.unwrap();
6834
6835 while let Some(Ok(msg)) = framed_read.next().await {
6837 if let Message::Request {
6838 index,
6839 begin,
6840 length,
6841 } = msg
6842 {
6843 let piece_data = seeder_storage.read_chunk(index, begin, length).unwrap();
6844 framed_write
6845 .send(Message::Piece {
6846 index,
6847 begin,
6848 data_0: Bytes::from(piece_data),
6849 data_1: Bytes::new(),
6850 })
6851 .await
6852 .unwrap();
6853 }
6854 }
6855 });
6856
6857 let leecher_storage = make_storage(&data, piece_length);
6859 let leecher_meta = make_test_torrent(&data, piece_length);
6860
6861 let leecher_config = test_config();
6862 let (latx, lamask) = test_alert_channel();
6863 let (ldh, ldm, _ldj) = test_register_disk(leecher_meta.info_hash, leecher_storage).await;
6864 let leecher = TorrentHandle::from_torrent(
6865 leecher_meta,
6866 irontide_core::TorrentVersion::V1Only,
6867 None,
6868 ldh,
6869 ldm,
6870 leecher_config,
6871 test_dht_rx(),
6872 test_dht_rx(),
6873 None,
6874 None,
6875 crate::slot_tuner::SlotTuner::disabled(4),
6876 latx,
6877 lamask,
6878 None,
6879 None,
6880 test_ban_manager(),
6881 test_ip_filter(),
6882 Arc::new(Vec::new()),
6883 None,
6884 None,
6885 Arc::new(crate::transport::NetworkFactory::tokio()),
6886 None, Arc::new(crate::stats::SessionCounters::new()),
6888 )
6889 .await
6890 .unwrap();
6891
6892 leecher
6894 .add_peers(vec![seeder_addr], PeerSource::Tracker)
6895 .await
6896 .unwrap();
6897
6898 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
6902 loop {
6903 tokio::time::sleep(Duration::from_millis(200)).await;
6904 let stats = leecher.stats().await.unwrap();
6905 if stats.state == TorrentState::Seeding {
6906 assert_eq!(stats.pieces_have, 2);
6907 assert_eq!(stats.pieces_total, 2);
6908 break;
6909 }
6910 if tokio::time::Instant::now() > deadline {
6911 let stats = leecher.stats().await.unwrap();
6912 panic!(
6913 "seeder/leecher: leecher did not complete, state={:?}, have={}/{}, connected={}, available={}",
6914 stats.state,
6915 stats.pieces_have,
6916 stats.pieces_total,
6917 stats.peers_connected,
6918 stats.peers_available,
6919 );
6920 }
6921 }
6922
6923 leecher.shutdown().await.unwrap();
6924 seeder_task.abort();
6925 }
6926
6927 #[tokio::test]
6930 async fn magnet_initial_stats() {
6931 let magnet = Magnet {
6932 info_hashes: irontide_core::InfoHashes::v1_only(
6933 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
6934 ),
6935 display_name: Some("magnet test".into()),
6936 trackers: vec![],
6937 peers: vec![],
6938 selected_files: None,
6939 };
6940
6941 let (atx, amask) = test_alert_channel();
6942 let (dm, _dj) = test_disk_manager();
6943 let handle = TorrentHandle::from_magnet(
6944 magnet,
6945 dm,
6946 test_config(),
6947 test_dht_rx(),
6948 test_dht_rx(),
6949 None,
6950 None,
6951 crate::slot_tuner::SlotTuner::disabled(4),
6952 atx,
6953 amask,
6954 None,
6955 None,
6956 test_ban_manager(),
6957 test_ip_filter(),
6958 Arc::new(Vec::new()),
6959 None,
6960 None,
6961 Arc::new(crate::transport::NetworkFactory::tokio()),
6962 None, Arc::new(crate::stats::SessionCounters::new()),
6964 )
6965 .await
6966 .unwrap();
6967
6968 let stats = handle.stats().await.unwrap();
6969 assert_eq!(stats.state, TorrentState::FetchingMetadata);
6970 assert_eq!(stats.pieces_total, 0);
6971 assert_eq!(stats.pieces_have, 0);
6972 assert_eq!(stats.downloaded, 0);
6973 assert_eq!(stats.uploaded, 0);
6974 assert_eq!(stats.peers_connected, 0);
6975 assert_eq!(stats.peers_available, 0);
6976
6977 handle.shutdown().await.unwrap();
6978 }
6979
6980 #[tokio::test]
6983 async fn tracker_populated_from_metadata() {
6984 use serde::Serialize;
6985
6986 #[derive(Serialize)]
6987 struct Info<'a> {
6988 length: u64,
6989 name: &'a str,
6990 #[serde(rename = "piece length")]
6991 piece_length: u64,
6992 #[serde(with = "serde_bytes")]
6993 pieces: &'a [u8],
6994 }
6995
6996 #[derive(Serialize)]
6997 struct Torrent<'a> {
6998 announce: &'a str,
6999 info: Info<'a>,
7000 }
7001
7002 let data = vec![0xAB; 16384];
7003 let hash = irontide_core::sha1(&data);
7004 let mut pieces = Vec::new();
7005 pieces.extend_from_slice(hash.as_bytes());
7006
7007 let t = Torrent {
7008 announce: "http://tracker.example.com:8080/announce",
7009 info: Info {
7010 length: data.len() as u64,
7011 name: "test",
7012 piece_length: 16384,
7013 pieces: &pieces,
7014 },
7015 };
7016
7017 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7018 let meta = torrent_from_bytes(&bytes).unwrap();
7019 assert!(meta.announce.is_some());
7020
7021 let storage = make_storage(&data, 16384);
7022 let config = test_config();
7023
7024 let (atx, amask) = test_alert_channel();
7027 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7028 let handle = TorrentHandle::from_torrent(
7029 meta,
7030 irontide_core::TorrentVersion::V1Only,
7031 None,
7032 dh,
7033 dm,
7034 config,
7035 test_dht_rx(),
7036 test_dht_rx(),
7037 None,
7038 None,
7039 crate::slot_tuner::SlotTuner::disabled(4),
7040 atx,
7041 amask,
7042 None,
7043 None,
7044 test_ban_manager(),
7045 test_ip_filter(),
7046 Arc::new(Vec::new()),
7047 None,
7048 None,
7049 Arc::new(crate::transport::NetworkFactory::tokio()),
7050 None, Arc::new(crate::stats::SessionCounters::new()),
7052 )
7053 .await
7054 .unwrap();
7055
7056 let stats = handle.stats().await.unwrap();
7057 assert_eq!(stats.state, TorrentState::Downloading);
7058
7059 handle.shutdown().await.unwrap();
7060 }
7061
7062 #[tokio::test]
7065 async fn private_torrent_no_dht_field() {
7066 use serde::Serialize;
7067
7068 #[derive(Serialize)]
7069 struct Info<'a> {
7070 length: u64,
7071 name: &'a str,
7072 #[serde(rename = "piece length")]
7073 piece_length: u64,
7074 #[serde(with = "serde_bytes")]
7075 pieces: &'a [u8],
7076 private: i64,
7077 }
7078
7079 #[derive(Serialize)]
7080 struct Torrent<'a> {
7081 announce: &'a str,
7082 info: Info<'a>,
7083 }
7084
7085 let data = vec![0xAB; 16384];
7086 let hash = irontide_core::sha1(&data);
7087 let mut pieces = Vec::new();
7088 pieces.extend_from_slice(hash.as_bytes());
7089
7090 let t = Torrent {
7091 announce: "http://private-tracker.example.com/announce",
7092 info: Info {
7093 length: data.len() as u64,
7094 name: "private_test",
7095 piece_length: 16384,
7096 pieces: &pieces,
7097 private: 1,
7098 },
7099 };
7100
7101 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7102 let meta = torrent_from_bytes(&bytes).unwrap();
7103 assert_eq!(meta.info.private, Some(1));
7104
7105 let storage = make_storage(&data, 16384);
7106 let config = test_config();
7107
7108 let (atx, amask) = test_alert_channel();
7109 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7110 let handle = TorrentHandle::from_torrent(
7111 meta,
7112 irontide_core::TorrentVersion::V1Only,
7113 None,
7114 dh,
7115 dm,
7116 config,
7117 test_dht_rx(),
7118 test_dht_rx(),
7119 None,
7120 None,
7121 crate::slot_tuner::SlotTuner::disabled(4),
7122 atx,
7123 amask,
7124 None,
7125 None,
7126 test_ban_manager(),
7127 test_ip_filter(),
7128 Arc::new(Vec::new()),
7129 None,
7130 None,
7131 Arc::new(crate::transport::NetworkFactory::tokio()),
7132 None, Arc::new(crate::stats::SessionCounters::new()),
7134 )
7135 .await
7136 .unwrap();
7137
7138 let stats = handle.stats().await.unwrap();
7139 assert_eq!(stats.state, TorrentState::Downloading);
7140
7141 handle.shutdown().await.unwrap();
7142 }
7143
7144 #[tokio::test]
7147 async fn magnet_no_tracker_before_metadata() {
7148 let magnet = Magnet {
7149 info_hashes: irontide_core::InfoHashes::v1_only(
7150 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
7151 ),
7152 display_name: Some("magnet test".into()),
7153 trackers: vec![],
7154 peers: vec![],
7155 selected_files: None,
7156 };
7157
7158 let (atx, amask) = test_alert_channel();
7159 let (dm, _dj) = test_disk_manager();
7160 let handle = TorrentHandle::from_magnet(
7161 magnet,
7162 dm,
7163 test_config(),
7164 test_dht_rx(),
7165 test_dht_rx(),
7166 None,
7167 None,
7168 crate::slot_tuner::SlotTuner::disabled(4),
7169 atx,
7170 amask,
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 let stats = handle.stats().await.unwrap();
7186 assert_eq!(stats.state, TorrentState::FetchingMetadata);
7187
7188 tokio::time::sleep(Duration::from_millis(50)).await;
7192
7193 handle.shutdown().await.unwrap();
7194 }
7195
7196 #[tokio::test]
7199 async fn pause_and_resume() {
7200 let data = vec![0xEEu8; 32768];
7201 let meta = make_test_torrent(&data, 16384);
7202 let storage = make_storage(&data, 16384);
7203 let config = test_config();
7204 let (atx, amask) = test_alert_channel();
7205 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7206 let handle = TorrentHandle::from_torrent(
7207 meta,
7208 irontide_core::TorrentVersion::V1Only,
7209 None,
7210 dh,
7211 dm,
7212 config,
7213 test_dht_rx(),
7214 test_dht_rx(),
7215 None,
7216 None,
7217 crate::slot_tuner::SlotTuner::disabled(4),
7218 atx,
7219 amask,
7220 None,
7221 None,
7222 test_ban_manager(),
7223 test_ip_filter(),
7224 Arc::new(Vec::new()),
7225 None,
7226 None,
7227 Arc::new(crate::transport::NetworkFactory::tokio()),
7228 None, Arc::new(crate::stats::SessionCounters::new()),
7230 )
7231 .await
7232 .unwrap();
7233
7234 let stats = handle.stats().await.unwrap();
7235 assert_eq!(stats.state, TorrentState::Downloading);
7236
7237 handle.pause().await.unwrap();
7238 tokio::time::sleep(Duration::from_millis(50)).await;
7239 let stats = handle.stats().await.unwrap();
7240 assert_eq!(stats.state, TorrentState::Paused);
7241
7242 handle.resume().await.unwrap();
7243 tokio::time::sleep(Duration::from_millis(50)).await;
7244 let stats = handle.stats().await.unwrap();
7245 assert_eq!(stats.state, TorrentState::Downloading);
7246
7247 handle.shutdown().await.unwrap();
7248 }
7249
7250 #[tokio::test]
7253 async fn pause_already_paused_is_noop() {
7254 let data = vec![0xEEu8; 32768];
7255 let meta = make_test_torrent(&data, 16384);
7256 let storage = make_storage(&data, 16384);
7257 let config = test_config();
7258 let (atx, amask) = test_alert_channel();
7259 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7260 let handle = TorrentHandle::from_torrent(
7261 meta,
7262 irontide_core::TorrentVersion::V1Only,
7263 None,
7264 dh,
7265 dm,
7266 config,
7267 test_dht_rx(),
7268 test_dht_rx(),
7269 None,
7270 None,
7271 crate::slot_tuner::SlotTuner::disabled(4),
7272 atx,
7273 amask,
7274 None,
7275 None,
7276 test_ban_manager(),
7277 test_ip_filter(),
7278 Arc::new(Vec::new()),
7279 None,
7280 None,
7281 Arc::new(crate::transport::NetworkFactory::tokio()),
7282 None, Arc::new(crate::stats::SessionCounters::new()),
7284 )
7285 .await
7286 .unwrap();
7287
7288 handle.pause().await.unwrap();
7289 tokio::time::sleep(Duration::from_millis(50)).await;
7290 handle.pause().await.unwrap(); tokio::time::sleep(Duration::from_millis(50)).await;
7292 let stats = handle.stats().await.unwrap();
7293 assert_eq!(stats.state, TorrentState::Paused);
7294
7295 handle.shutdown().await.unwrap();
7296 }
7297
7298 #[tokio::test]
7304 async fn incoming_request_served_from_storage() {
7305 let data = vec![0xABu8; 16384];
7306 let meta = make_test_torrent(&data, 16384);
7307 let info_hash = meta.info_hash;
7308 let storage = make_storage(&data, 16384);
7309
7310 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7311 let listen_addr = listener.local_addr().unwrap();
7312 drop(listener);
7313
7314 let config = TorrentConfig {
7315 listen_port: listen_addr.port(),
7316 ..test_config()
7317 };
7318
7319 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 tokio::time::sleep(Duration::from_millis(50)).await;
7350
7351 let seed_data = data.clone();
7353 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7354 let seeder_task = tokio::spawn(async move {
7355 let (reader, writer) = tokio::io::split(seed_stream);
7356 let mut writer = writer;
7357 let mut reader = reader;
7358
7359 let hs = Handshake::new(
7360 info_hash,
7361 Id20::from_hex("6666666666666666666666666666666666666666").unwrap(),
7362 );
7363 writer.write_all(&hs.to_bytes()).await.unwrap();
7364 writer.flush().await.unwrap();
7365 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7366 reader.read_exact(&mut hs_buf).await.unwrap();
7367
7368 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7369 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7370
7371 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
7373 let payload = ext_hs.to_bytes().unwrap();
7374 framed_write
7375 .send(Message::Extended { ext_id: 0, payload })
7376 .await
7377 .unwrap();
7378
7379 let mut bf = Bitfield::new(1);
7381 bf.set(0);
7382 framed_write
7383 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7384 .await
7385 .unwrap();
7386 framed_write.send(Message::Unchoke).await.unwrap();
7387
7388 while let Some(Ok(msg)) = framed_read.next().await {
7390 if let Message::Request {
7391 index,
7392 begin,
7393 length,
7394 } = msg
7395 {
7396 let start = begin as usize;
7397 let end = start + length as usize;
7398 framed_write
7399 .send(Message::Piece {
7400 index,
7401 begin,
7402 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
7403 data_1: Bytes::new(),
7404 })
7405 .await
7406 .unwrap();
7407 }
7408 }
7409 });
7410
7411 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7413 loop {
7414 tokio::time::sleep(Duration::from_millis(100)).await;
7415 let stats = handle.stats().await.unwrap();
7416 if stats.pieces_have == 1 {
7417 break;
7418 }
7419 assert!(
7420 tokio::time::Instant::now() <= deadline,
7421 "piece download did not complete within 5s"
7422 );
7423 }
7424
7425 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7427 let expected_data = data.clone();
7428 let leecher_task = tokio::spawn(async move {
7429 let (reader, writer) = tokio::io::split(leech_stream);
7430 let mut writer = writer;
7431 let mut reader = reader;
7432
7433 let hs = Handshake::new(
7434 info_hash,
7435 Id20::from_hex("7777777777777777777777777777777777777777").unwrap(),
7436 );
7437 writer.write_all(&hs.to_bytes()).await.unwrap();
7438 writer.flush().await.unwrap();
7439 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7440 reader.read_exact(&mut hs_buf).await.unwrap();
7441
7442 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7443 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7444
7445 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
7447 let payload = ext_hs.to_bytes().unwrap();
7448 framed_write
7449 .send(Message::Extended { ext_id: 0, payload })
7450 .await
7451 .unwrap();
7452
7453 framed_write.send(Message::Interested).await.unwrap();
7455
7456 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
7457 loop {
7458 tokio::select! {
7459 msg = framed_read.next() => {
7460 match msg {
7461 Some(Ok(Message::Unchoke)) => { break; }
7462 Some(Ok(_)) => {}
7463 _ => panic!("connection closed before unchoke"),
7464 }
7465 }
7466 () = tokio::time::sleep_until(deadline) => {
7467 panic!("timed out waiting for unchoke");
7468 }
7469 }
7470 }
7471
7472 framed_write
7474 .send(Message::Request {
7475 index: 0,
7476 begin: 0,
7477 length: 16384,
7478 })
7479 .await
7480 .unwrap();
7481
7482 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7484 loop {
7485 tokio::select! {
7486 msg = framed_read.next() => {
7487 match msg {
7488 Some(Ok(Message::Piece { index, begin, data_0, data_1 })) => {
7489 assert_eq!(index, 0);
7490 assert_eq!(begin, 0);
7491 let _ = &data_1; assert_eq!(data_0.as_ref(), expected_data.as_slice());
7493 return; }
7495 Some(Ok(_)) => {}
7496 Some(Err(e)) => panic!("error reading: {e}"),
7497 None => panic!("connection closed before piece"),
7498 }
7499 }
7500 () = tokio::time::sleep_until(deadline) => {
7501 panic!("timed out waiting for piece data");
7502 }
7503 }
7504 }
7505 });
7506
7507 let result = tokio::time::timeout(Duration::from_secs(20), leecher_task).await;
7509 match result {
7510 Ok(Ok(())) => {}
7511 Ok(Err(e)) => panic!("leecher task panicked: {e}"),
7512 Err(elapsed) => panic!("test timed out after {elapsed}"),
7513 }
7514
7515 let stats = handle.stats().await.unwrap();
7517 assert!(
7518 stats.uploaded > 0,
7519 "expected uploaded > 0, got {}",
7520 stats.uploaded
7521 );
7522
7523 handle.shutdown().await.unwrap();
7524 seeder_task.abort();
7525 }
7526
7527 #[tokio::test]
7530 async fn seed_ratio_limit_stops_torrent() {
7531 let data = vec![0xCCu8; 16384];
7534 let meta = make_test_torrent(&data, 16384);
7535 let info_hash = meta.info_hash;
7536 let storage = make_storage(&data, 16384);
7537
7538 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7539 let listen_addr = listener.local_addr().unwrap();
7540 drop(listener);
7541
7542 let config = TorrentConfig {
7543 listen_port: listen_addr.port(),
7544 seed_ratio_limit: Some(1.0),
7545 ..test_config()
7546 };
7547
7548 let (atx, amask) = test_alert_channel();
7549 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7550 let handle = TorrentHandle::from_torrent(
7551 meta,
7552 irontide_core::TorrentVersion::V1Only,
7553 None,
7554 dh,
7555 dm,
7556 config,
7557 test_dht_rx(),
7558 test_dht_rx(),
7559 None,
7560 None,
7561 crate::slot_tuner::SlotTuner::disabled(4),
7562 atx,
7563 amask,
7564 None,
7565 None,
7566 test_ban_manager(),
7567 test_ip_filter(),
7568 Arc::new(Vec::new()),
7569 None,
7570 None,
7571 Arc::new(crate::transport::NetworkFactory::tokio()),
7572 None, Arc::new(crate::stats::SessionCounters::new()),
7574 )
7575 .await
7576 .unwrap();
7577
7578 tokio::time::sleep(Duration::from_millis(50)).await;
7579
7580 let seed_data = data.clone();
7582 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7583 let seeder_task = tokio::spawn(async move {
7584 let (reader, writer) = tokio::io::split(seed_stream);
7585 let mut writer = writer;
7586 let mut reader = reader;
7587
7588 let hs = Handshake::new(
7589 info_hash,
7590 Id20::from_hex("8888888888888888888888888888888888888888").unwrap(),
7591 );
7592 writer.write_all(&hs.to_bytes()).await.unwrap();
7593 writer.flush().await.unwrap();
7594 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7595 reader.read_exact(&mut hs_buf).await.unwrap();
7596
7597 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7598 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7599
7600 let _msg = framed_read.next().await;
7601 let ext_hs = ExtHandshake::new();
7602 let payload = ext_hs.to_bytes().unwrap();
7603 framed_write
7604 .send(Message::Extended { ext_id: 0, payload })
7605 .await
7606 .unwrap();
7607
7608 let mut bf = Bitfield::new(1);
7609 bf.set(0);
7610 framed_write
7611 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7612 .await
7613 .unwrap();
7614 framed_write.send(Message::Unchoke).await.unwrap();
7615
7616 while let Some(Ok(msg)) = framed_read.next().await {
7617 if let Message::Request {
7618 index,
7619 begin,
7620 length,
7621 } = msg
7622 {
7623 let start = begin as usize;
7624 let end = start + length as usize;
7625 framed_write
7626 .send(Message::Piece {
7627 index,
7628 begin,
7629 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
7630 data_1: Bytes::new(),
7631 })
7632 .await
7633 .unwrap();
7634 }
7635 }
7636 });
7637
7638 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7640 loop {
7641 tokio::time::sleep(Duration::from_millis(100)).await;
7642 let stats = handle.stats().await.unwrap();
7643 if stats.state == TorrentState::Seeding {
7644 break;
7645 }
7646 assert!(
7647 tokio::time::Instant::now() <= deadline,
7648 "download did not complete within 5s"
7649 );
7650 }
7651
7652 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7654 let leecher_task = tokio::spawn(async move {
7655 let (reader, writer) = tokio::io::split(leech_stream);
7656 let mut writer = writer;
7657 let mut reader = reader;
7658
7659 let hs = Handshake::new(
7660 info_hash,
7661 Id20::from_hex("9999999999999999999999999999999999999999").unwrap(),
7662 );
7663 writer.write_all(&hs.to_bytes()).await.unwrap();
7664 writer.flush().await.unwrap();
7665 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7666 reader.read_exact(&mut hs_buf).await.unwrap();
7667
7668 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7669 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7670
7671 let _msg = framed_read.next().await;
7672 let ext_hs = ExtHandshake::new();
7673 let payload = ext_hs.to_bytes().unwrap();
7674 framed_write
7675 .send(Message::Extended { ext_id: 0, payload })
7676 .await
7677 .unwrap();
7678
7679 framed_write.send(Message::Interested).await.unwrap();
7680
7681 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
7683 loop {
7684 tokio::select! {
7685 msg = framed_read.next() => {
7686 match msg {
7687 Some(Ok(Message::Unchoke)) => break,
7688 Some(Ok(_)) => {}
7689 _ => return, }
7691 }
7692 () = tokio::time::sleep_until(deadline) => return,
7693 }
7694 }
7695
7696 framed_write
7698 .send(Message::Request {
7699 index: 0,
7700 begin: 0,
7701 length: 16384,
7702 })
7703 .await
7704 .unwrap();
7705
7706 while let Some(Ok(_msg)) = framed_read.next().await {}
7708 });
7709
7710 let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
7712 loop {
7713 tokio::time::sleep(Duration::from_millis(100)).await;
7714 let stats = handle.stats().await.unwrap();
7715 if stats.state == TorrentState::Stopped {
7716 assert!(
7717 stats.uploaded >= 16384,
7718 "expected uploaded >= 16384, got {}",
7719 stats.uploaded
7720 );
7721 break;
7722 }
7723 if tokio::time::Instant::now() > deadline {
7724 let stats = handle.stats().await.unwrap();
7725 panic!(
7726 "expected Stopped, got {:?}, uploaded={}, downloaded={}",
7727 stats.state, stats.uploaded, stats.downloaded
7728 );
7729 }
7730 }
7731
7732 handle.shutdown().await.unwrap();
7733 seeder_task.abort();
7734 leecher_task.abort();
7735 }
7736
7737 #[tokio::test]
7740 async fn resume_with_seeded_storage() {
7741 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
7743 let storage = make_seeded_storage(&data, 16384);
7744 let config = test_config();
7745
7746 let (atx, amask) = test_alert_channel();
7747 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7748 let handle = TorrentHandle::from_torrent(
7749 meta,
7750 irontide_core::TorrentVersion::V1Only,
7751 None,
7752 dh,
7753 dm,
7754 config,
7755 test_dht_rx(),
7756 test_dht_rx(),
7757 None,
7758 None,
7759 crate::slot_tuner::SlotTuner::disabled(4),
7760 atx,
7761 amask,
7762 None,
7763 None,
7764 test_ban_manager(),
7765 test_ip_filter(),
7766 Arc::new(Vec::new()),
7767 None,
7768 None,
7769 Arc::new(crate::transport::NetworkFactory::tokio()),
7770 None, Arc::new(crate::stats::SessionCounters::new()),
7772 )
7773 .await
7774 .unwrap();
7775
7776 tokio::time::sleep(Duration::from_millis(100)).await;
7778
7779 let stats = handle.stats().await.unwrap();
7780 assert_eq!(
7781 stats.state,
7782 TorrentState::Seeding,
7783 "should start as seeder with all pieces verified"
7784 );
7785 assert_eq!(stats.pieces_have, 2);
7786 assert_eq!(stats.pieces_total, 2);
7787
7788 handle.shutdown().await.unwrap();
7789 }
7790
7791 #[tokio::test]
7794 async fn save_resume_data_captures_state() {
7795 let data = vec![0xAB; 32768];
7796 let meta = make_test_torrent(&data, 16384);
7797 let info_hash = meta.info_hash;
7798 let storage = make_storage(&data, 16384);
7799 let config = test_config();
7800
7801 let (atx, amask) = test_alert_channel();
7802 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7803 let handle = TorrentHandle::from_torrent(
7804 meta,
7805 irontide_core::TorrentVersion::V1Only,
7806 None,
7807 dh,
7808 dm,
7809 config,
7810 test_dht_rx(),
7811 test_dht_rx(),
7812 None,
7813 None,
7814 crate::slot_tuner::SlotTuner::disabled(4),
7815 atx,
7816 amask,
7817 None,
7818 None,
7819 test_ban_manager(),
7820 test_ip_filter(),
7821 Arc::new(Vec::new()),
7822 None,
7823 None,
7824 Arc::new(crate::transport::NetworkFactory::tokio()),
7825 None, Arc::new(crate::stats::SessionCounters::new()),
7827 )
7828 .await
7829 .unwrap();
7830
7831 tokio::time::sleep(Duration::from_millis(50)).await;
7833
7834 let rd = handle.save_resume_data().await.unwrap();
7835
7836 assert_eq!(rd.file_format, "libtorrent resume file");
7837 assert_eq!(rd.file_version, 1);
7838 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
7839 assert_eq!(rd.name, "test");
7840 assert_eq!(rd.save_path, "/tmp");
7841 assert_eq!(rd.paused, 0);
7842 assert!(!rd.pieces.is_empty());
7844 assert_eq!(rd.total_uploaded, 0);
7846 assert_eq!(rd.total_downloaded, 0);
7847
7848 handle.shutdown().await.unwrap();
7849 }
7850
7851 #[tokio::test]
7854 async fn save_resume_data_seeder() {
7855 let data = vec![0xCD; 32768];
7856 let meta = make_test_torrent(&data, 16384);
7857 let info_hash = meta.info_hash;
7858 let storage = make_seeded_storage(&data, 16384);
7859 let config = test_config();
7860
7861 let (atx, amask) = test_alert_channel();
7862 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7863 let handle = TorrentHandle::from_torrent(
7864 meta,
7865 irontide_core::TorrentVersion::V1Only,
7866 None,
7867 dh,
7868 dm,
7869 config,
7870 test_dht_rx(),
7871 test_dht_rx(),
7872 None,
7873 None,
7874 crate::slot_tuner::SlotTuner::disabled(4),
7875 atx,
7876 amask,
7877 None,
7878 None,
7879 test_ban_manager(),
7880 test_ip_filter(),
7881 Arc::new(Vec::new()),
7882 None,
7883 None,
7884 Arc::new(crate::transport::NetworkFactory::tokio()),
7885 None, Arc::new(crate::stats::SessionCounters::new()),
7887 )
7888 .await
7889 .unwrap();
7890
7891 tokio::time::sleep(Duration::from_millis(100)).await;
7893
7894 let rd = handle.save_resume_data().await.unwrap();
7895
7896 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
7897 assert_eq!(rd.name, "test");
7898 assert_eq!(rd.seed_mode, 1, "seeder should have seed_mode=1");
7899 assert_eq!(rd.paused, 0);
7900 assert_eq!(rd.pieces.len(), 1);
7903 assert_eq!(
7904 rd.pieces[0] & 0xC0,
7905 0xC0,
7906 "both pieces should be marked complete"
7907 );
7908
7909 handle.shutdown().await.unwrap();
7910 }
7911
7912 #[tokio::test]
7915 async fn save_resume_data_paused() {
7916 let data = vec![0xEF; 16384];
7917 let meta = make_test_torrent(&data, 16384);
7918 let storage = make_storage(&data, 16384);
7919 let config = test_config();
7920
7921 let (atx, amask) = test_alert_channel();
7922 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7923 let handle = TorrentHandle::from_torrent(
7924 meta,
7925 irontide_core::TorrentVersion::V1Only,
7926 None,
7927 dh,
7928 dm,
7929 config,
7930 test_dht_rx(),
7931 test_dht_rx(),
7932 None,
7933 None,
7934 crate::slot_tuner::SlotTuner::disabled(4),
7935 atx,
7936 amask,
7937 None,
7938 None,
7939 test_ban_manager(),
7940 test_ip_filter(),
7941 Arc::new(Vec::new()),
7942 None,
7943 None,
7944 Arc::new(crate::transport::NetworkFactory::tokio()),
7945 None, Arc::new(crate::stats::SessionCounters::new()),
7947 )
7948 .await
7949 .unwrap();
7950
7951 tokio::time::sleep(Duration::from_millis(50)).await;
7952 handle.pause().await.unwrap();
7953 tokio::time::sleep(Duration::from_millis(50)).await;
7954
7955 let rd = handle.save_resume_data().await.unwrap();
7956 assert_eq!(rd.paused, 1, "paused torrent should have paused=1");
7957 assert_eq!(rd.seed_mode, 0);
7958
7959 handle.shutdown().await.unwrap();
7960 }
7961
7962 #[tokio::test]
7965 async fn set_file_priority_and_read_back() {
7966 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
7967 let mut torrent_bytes = b"d4:info".to_vec();
7968 torrent_bytes.extend_from_slice(info_bytes);
7969 torrent_bytes.push(b'e');
7970
7971 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
7972 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
7973 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
7974 let config = TorrentConfig {
7975 listen_port: 0,
7976 ..Default::default()
7977 };
7978
7979 let (atx, amask) = test_alert_channel();
7980 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7981 let handle = TorrentHandle::from_torrent(
7982 meta,
7983 irontide_core::TorrentVersion::V1Only,
7984 None,
7985 dh,
7986 dm,
7987 config,
7988 test_dht_rx(),
7989 test_dht_rx(),
7990 None,
7991 None,
7992 crate::slot_tuner::SlotTuner::disabled(4),
7993 atx,
7994 amask,
7995 None,
7996 None,
7997 test_ban_manager(),
7998 test_ip_filter(),
7999 Arc::new(Vec::new()),
8000 None,
8001 None,
8002 Arc::new(crate::transport::NetworkFactory::tokio()),
8003 None, Arc::new(crate::stats::SessionCounters::new()),
8005 )
8006 .await
8007 .unwrap();
8008
8009 let prios = handle.file_priorities().await.unwrap();
8011 assert_eq!(prios.len(), 2);
8012 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
8013
8014 handle
8016 .set_file_priority(0, FilePriority::Skip)
8017 .await
8018 .unwrap();
8019
8020 let prios = handle.file_priorities().await.unwrap();
8021 assert_eq!(prios[0], FilePriority::Skip);
8022 assert_eq!(prios[1], FilePriority::Normal);
8023
8024 let result = handle.set_file_priority(99, FilePriority::High).await;
8026 assert!(result.is_err());
8027
8028 handle.shutdown().await.unwrap();
8029 tokio::time::sleep(Duration::from_millis(50)).await;
8030 }
8031
8032 #[tokio::test]
8033 async fn resume_data_preserves_file_priorities() {
8034 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
8035 let mut torrent_bytes = b"d4:info".to_vec();
8036 torrent_bytes.extend_from_slice(info_bytes);
8037 torrent_bytes.push(b'e');
8038
8039 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
8040 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
8041 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8042 let config = TorrentConfig {
8043 listen_port: 0,
8044 ..Default::default()
8045 };
8046
8047 let (atx, amask) = test_alert_channel();
8048 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8049 let handle = TorrentHandle::from_torrent(
8050 meta,
8051 irontide_core::TorrentVersion::V1Only,
8052 None,
8053 dh,
8054 dm,
8055 config,
8056 test_dht_rx(),
8057 test_dht_rx(),
8058 None,
8059 None,
8060 crate::slot_tuner::SlotTuner::disabled(4),
8061 atx,
8062 amask,
8063 None,
8064 None,
8065 test_ban_manager(),
8066 test_ip_filter(),
8067 Arc::new(Vec::new()),
8068 None,
8069 None,
8070 Arc::new(crate::transport::NetworkFactory::tokio()),
8071 None, Arc::new(crate::stats::SessionCounters::new()),
8073 )
8074 .await
8075 .unwrap();
8076
8077 handle
8079 .set_file_priority(0, FilePriority::High)
8080 .await
8081 .unwrap();
8082 handle
8083 .set_file_priority(1, FilePriority::Skip)
8084 .await
8085 .unwrap();
8086
8087 let rd = handle.save_resume_data().await.unwrap();
8089 assert_eq!(rd.file_priority, vec![7, 0]); let encoded = irontide_bencode::to_bytes(&rd).unwrap();
8093 let decoded: irontide_core::FastResumeData =
8094 irontide_bencode::from_bytes(&encoded).unwrap();
8095 assert_eq!(decoded.file_priority, vec![7, 0]);
8096
8097 handle.shutdown().await.unwrap();
8098 tokio::time::sleep(Duration::from_millis(50)).await;
8099 }
8100
8101 #[tokio::test]
8104 async fn upload_rate_limiting_caps_throughput() {
8105 let data = vec![0xAB; 16384]; let meta = make_test_torrent(&data, 16384);
8111 let info_hash = meta.info_hash;
8112 let storage = make_seeded_storage(&data, 16384);
8113
8114 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8115 let listen_addr = listener.local_addr().unwrap();
8116
8117 let config = TorrentConfig {
8118 listen_port: listen_addr.port(),
8119 upload_rate_limit: 1024, ..test_config()
8121 };
8122
8123 drop(listener);
8124 let (atx, amask) = test_alert_channel();
8125 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8126 let handle = TorrentHandle::from_torrent(
8127 meta,
8128 irontide_core::TorrentVersion::V1Only,
8129 None,
8130 dh,
8131 dm,
8132 config,
8133 test_dht_rx(),
8134 test_dht_rx(),
8135 None,
8136 None,
8137 crate::slot_tuner::SlotTuner::disabled(4),
8138 atx,
8139 amask,
8140 None,
8141 None,
8142 test_ban_manager(),
8143 test_ip_filter(),
8144 Arc::new(Vec::new()),
8145 None,
8146 None,
8147 Arc::new(crate::transport::NetworkFactory::tokio()),
8148 None, Arc::new(crate::stats::SessionCounters::new()),
8150 )
8151 .await
8152 .unwrap();
8153
8154 tokio::time::sleep(Duration::from_millis(50)).await;
8155
8156 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8158 let (reader, writer) = tokio::io::split(stream);
8159 let mut writer = writer;
8160 let mut reader = reader;
8161
8162 let hs = Handshake::new(
8163 info_hash,
8164 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
8165 );
8166 writer.write_all(&hs.to_bytes()).await.unwrap();
8167 writer.flush().await.unwrap();
8168 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8169 reader.read_exact(&mut hs_buf).await.unwrap();
8170
8171 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8172 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8173
8174 let _msg = framed_read.next().await;
8176 let ext_hs = ExtHandshake::new();
8177 let payload = ext_hs.to_bytes().unwrap();
8178 framed_write
8179 .send(Message::Extended { ext_id: 0, payload })
8180 .await
8181 .unwrap();
8182
8183 let _bf_msg = framed_read.next().await;
8185
8186 framed_write.send(Message::Interested).await.unwrap();
8188
8189 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
8191 loop {
8192 tokio::select! {
8193 msg = framed_read.next() => {
8194 match msg {
8195 Some(Ok(Message::Unchoke)) => break,
8196 Some(Ok(_)) => {}
8197 _ => panic!("connection closed before unchoke"),
8198 }
8199 }
8200 () = tokio::time::sleep_until(deadline) => {
8201 panic!("timed out waiting for unchoke");
8202 }
8203 }
8204 }
8205
8206 framed_write
8208 .send(Message::Request {
8209 index: 0,
8210 begin: 0,
8211 length: 16384,
8212 })
8213 .await
8214 .unwrap();
8215
8216 let mut got_piece = false;
8220 if let Ok(true) = tokio::time::timeout(Duration::from_secs(2), async {
8221 loop {
8222 match framed_read.next().await {
8223 Some(Ok(Message::Piece { .. })) => return true,
8224 Some(Ok(_)) => {}
8225 _ => return false,
8226 }
8227 }
8228 })
8229 .await
8230 {
8231 got_piece = true;
8232 }
8233
8234 assert!(
8236 !got_piece,
8237 "piece should be delayed by rate limiter (1 KB/s for 16 KB chunk)"
8238 );
8239
8240 let stats = handle.stats().await.unwrap();
8242 assert_eq!(stats.uploaded, 0); handle.shutdown().await.unwrap();
8245 }
8246
8247 #[tokio::test]
8248 async fn unlimited_rate_has_no_effect() {
8249 let data = vec![0xAB; 32768];
8251 let meta = make_test_torrent(&data, 16384);
8252 let storage = make_storage(&data, 16384);
8253 let config = test_config();
8254
8255 assert_eq!(config.upload_rate_limit, 0);
8257 assert_eq!(config.download_rate_limit, 0);
8258
8259 let (atx, amask) = test_alert_channel();
8260 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8261 let handle = TorrentHandle::from_torrent(
8262 meta,
8263 irontide_core::TorrentVersion::V1Only,
8264 None,
8265 dh,
8266 dm,
8267 config,
8268 test_dht_rx(),
8269 test_dht_rx(),
8270 None,
8271 None,
8272 crate::slot_tuner::SlotTuner::disabled(4),
8273 atx,
8274 amask,
8275 None,
8276 None,
8277 test_ban_manager(),
8278 test_ip_filter(),
8279 Arc::new(Vec::new()),
8280 None,
8281 None,
8282 Arc::new(crate::transport::NetworkFactory::tokio()),
8283 None, Arc::new(crate::stats::SessionCounters::new()),
8285 )
8286 .await
8287 .unwrap();
8288
8289 let stats = handle.stats().await.unwrap();
8290 assert_eq!(stats.state, TorrentState::Downloading);
8291 assert_eq!(stats.pieces_total, 2);
8292
8293 handle.shutdown().await.unwrap();
8294 }
8295
8296 #[tokio::test]
8297 async fn download_rate_limiting_throttles_requests() {
8298 let data = vec![0xAB; 32768];
8301 let meta = make_test_torrent(&data, 16384);
8302 let info_hash = meta.info_hash;
8303 let storage = make_storage(&data, 16384);
8304
8305 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8306 let listen_addr = listener.local_addr().unwrap();
8307
8308 let config = TorrentConfig {
8309 listen_port: listen_addr.port(),
8310 download_rate_limit: 1024, ..test_config()
8312 };
8313
8314 drop(listener);
8315 let (atx, amask) = test_alert_channel();
8316 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8317 let handle = TorrentHandle::from_torrent(
8318 meta,
8319 irontide_core::TorrentVersion::V1Only,
8320 None,
8321 dh,
8322 dm,
8323 config,
8324 test_dht_rx(),
8325 test_dht_rx(),
8326 None,
8327 None,
8328 crate::slot_tuner::SlotTuner::disabled(4),
8329 atx,
8330 amask,
8331 None,
8332 None,
8333 test_ban_manager(),
8334 test_ip_filter(),
8335 Arc::new(Vec::new()),
8336 None,
8337 None,
8338 Arc::new(crate::transport::NetworkFactory::tokio()),
8339 None, Arc::new(crate::stats::SessionCounters::new()),
8341 )
8342 .await
8343 .unwrap();
8344
8345 tokio::time::sleep(Duration::from_millis(50)).await;
8346
8347 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8349 let (reader, writer) = tokio::io::split(stream);
8350 let mut writer = writer;
8351 let mut reader = reader;
8352
8353 let hs = Handshake::new(
8354 info_hash,
8355 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
8356 );
8357 writer.write_all(&hs.to_bytes()).await.unwrap();
8358 writer.flush().await.unwrap();
8359 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8360 reader.read_exact(&mut hs_buf).await.unwrap();
8361
8362 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8363 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8364
8365 let _msg = framed_read.next().await;
8367 let ext_hs = ExtHandshake::new();
8368 let payload = ext_hs.to_bytes().unwrap();
8369 framed_write
8370 .send(Message::Extended { ext_id: 0, payload })
8371 .await
8372 .unwrap();
8373
8374 let mut bf = Bitfield::new(2);
8376 bf.set(0);
8377 bf.set(1);
8378 framed_write
8379 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
8380 .await
8381 .unwrap();
8382
8383 framed_write.send(Message::Unchoke).await.unwrap();
8385
8386 let mut requests_received = 0u32;
8390 let deadline = tokio::time::Instant::now() + Duration::from_millis(500);
8391 loop {
8392 match tokio::time::timeout(
8393 deadline.saturating_duration_since(tokio::time::Instant::now()),
8394 framed_read.next(),
8395 )
8396 .await
8397 {
8398 Ok(Some(Ok(Message::Request { .. }))) => {
8399 requests_received += 1;
8400 }
8401 Ok(Some(Ok(_))) => {}
8402 _ => break,
8403 }
8404 }
8405
8406 let stats = handle.stats().await.unwrap();
8407 assert_eq!(stats.state, TorrentState::Downloading);
8408
8409 assert!(
8412 requests_received <= 2,
8413 "with 1 KB/s limit, should get very few requests, got {requests_received}"
8414 );
8415
8416 handle.shutdown().await.unwrap();
8417 }
8418
8419 #[test]
8422 fn piece_contributor_tracking() {
8423 use std::net::IpAddr;
8424 let mut contributors: HashMap<u32, HashSet<IpAddr>> = HashMap::new();
8425 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
8426 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
8427
8428 contributors.entry(0).or_default().insert(ip1);
8429 contributors.entry(0).or_default().insert(ip2);
8430 assert_eq!(contributors[&0].len(), 2);
8431 assert!(contributors[&0].contains(&ip1));
8432 assert!(contributors[&0].contains(&ip2));
8433
8434 contributors.remove(&0);
8436 assert!(!contributors.contains_key(&0));
8437 }
8438
8439 #[test]
8440 fn parole_enter_on_hash_failure() {
8441 use crate::ban::ParoleState;
8442 use std::net::IpAddr;
8443
8444 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
8445 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
8446 let contributors = vec![ip1, ip2];
8447
8448 let parole = ParoleState {
8450 original_contributors: contributors.into_iter().collect(),
8451 parole_peer: None,
8452 };
8453
8454 assert_eq!(parole.original_contributors.len(), 2);
8455 assert!(parole.original_contributors.contains(&ip1));
8456 assert!(parole.original_contributors.contains(&ip2));
8457 assert!(parole.parole_peer.is_none());
8458 }
8459
8460 #[test]
8461 fn parole_success_strikes_originals() {
8462 use crate::ban::{BanConfig, BanManager, ParoleState};
8463 use std::net::IpAddr;
8464
8465 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
8466 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
8467 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
8468
8469 let mut mgr = BanManager::new(BanConfig {
8470 max_failures: 2,
8471 use_parole: true,
8472 });
8473
8474 let parole = ParoleState {
8475 original_contributors: [ip1, ip2].into_iter().collect(),
8476 parole_peer: Some(parole_ip),
8477 };
8478
8479 for ip in &parole.original_contributors {
8481 mgr.record_strike(*ip);
8482 }
8483
8484 assert_eq!(*mgr.strikes_map().get(&ip1).unwrap(), 1);
8485 assert_eq!(*mgr.strikes_map().get(&ip2).unwrap(), 1);
8486 assert!(!mgr.strikes_map().contains_key(&parole_ip));
8488
8489 for ip in &parole.original_contributors {
8491 mgr.record_strike(*ip);
8492 }
8493 assert!(mgr.is_banned(&ip1));
8494 assert!(mgr.is_banned(&ip2));
8495 }
8496
8497 #[test]
8498 fn parole_failure_strikes_parole_peer() {
8499 use crate::ban::{BanConfig, BanManager, ParoleState};
8500 use std::net::IpAddr;
8501
8502 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
8503 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
8504
8505 let mut mgr = BanManager::new(BanConfig {
8506 max_failures: 2,
8507 use_parole: true,
8508 });
8509
8510 let parole = ParoleState {
8511 original_contributors: [ip1].into_iter().collect(),
8512 parole_peer: Some(parole_ip),
8513 };
8514
8515 if let Some(pp) = parole.parole_peer {
8517 mgr.record_strike(pp);
8518 }
8519
8520 assert_eq!(*mgr.strikes_map().get(&parole_ip).unwrap(), 1);
8521 assert!(!mgr.strikes_map().contains_key(&ip1));
8522 }
8523
8524 #[tokio::test]
8525 async fn banned_peer_rejected_on_connect() {
8526 let data = vec![0xAB; 32768];
8527 let meta = make_test_torrent(&data, 16384);
8528 let storage = make_storage(&data, 16384);
8529 let config = test_config();
8530 let ban_mgr = test_ban_manager();
8531
8532 let banned_ip: std::net::IpAddr = "192.168.1.100".parse().unwrap();
8534 ban_mgr.write().ban(banned_ip);
8535
8536 let (atx, amask) = test_alert_channel();
8537 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8538 let handle = TorrentHandle::from_torrent(
8539 meta,
8540 irontide_core::TorrentVersion::V1Only,
8541 None,
8542 dh,
8543 dm,
8544 config,
8545 test_dht_rx(),
8546 test_dht_rx(),
8547 None,
8548 None,
8549 crate::slot_tuner::SlotTuner::disabled(4),
8550 atx,
8551 amask,
8552 None,
8553 None,
8554 Arc::clone(&ban_mgr),
8555 test_ip_filter(),
8556 Arc::new(Vec::new()),
8557 None,
8558 None,
8559 Arc::new(crate::transport::NetworkFactory::tokio()),
8560 None, Arc::new(crate::stats::SessionCounters::new()),
8562 )
8563 .await
8564 .unwrap();
8565
8566 handle
8568 .add_peers(
8569 vec![
8570 SocketAddr::new(banned_ip, 6881),
8571 "10.0.0.1:6881".parse().unwrap(),
8572 ],
8573 PeerSource::Tracker,
8574 )
8575 .await
8576 .unwrap();
8577
8578 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
8579 let stats = handle.stats().await.unwrap();
8580 assert!(
8583 stats.peers_available + stats.peers_connected <= 1,
8584 "banned peer should not be added: available={}, connected={}",
8585 stats.peers_available,
8586 stats.peers_connected
8587 );
8588
8589 handle.shutdown().await.unwrap();
8590 }
8591
8592 #[test]
8593 fn banned_peer_filtered_from_available() {
8594 use crate::ban::{BanConfig, BanManager};
8595 use std::net::IpAddr;
8596
8597 let banned_ip: IpAddr = "192.168.1.200".parse().unwrap();
8598 let ok_ip: IpAddr = "10.0.0.1".parse().unwrap();
8599
8600 let mgr = BanManager::new(BanConfig::default());
8601 assert!(!mgr.is_banned(&banned_ip));
8603 assert!(!mgr.is_banned(&ok_ip));
8604
8605 let mut mgr = BanManager::new(BanConfig::default());
8606 mgr.ban(banned_ip);
8607
8608 assert!(mgr.is_banned(&banned_ip));
8610 assert!(!mgr.is_banned(&ok_ip));
8611 }
8612
8613 #[test]
8616 fn hashing_threads_config_default() {
8617 let s = crate::settings::Settings::default();
8618 let expected = {
8619 let cores = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
8620 (cores / 4).clamp(2, 8)
8621 };
8622 assert_eq!(s.hashing_threads, expected);
8623 let tc = TorrentConfig::default();
8624 assert_eq!(tc.hashing_threads, expected);
8625 }
8626
8627 #[tokio::test]
8628 async fn checking_state_and_progress_alerts() {
8629 use crate::alert::AlertKind;
8630
8631 let data = vec![0xEEu8; 65536]; let meta = make_test_torrent(&data, 16384);
8633 let storage = make_seeded_storage(&data, 16384);
8634 let config = test_config();
8635
8636 let (atx, amask) = test_alert_channel();
8637 let mut rx = atx.subscribe();
8638 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8639 let handle = TorrentHandle::from_torrent(
8640 meta,
8641 irontide_core::TorrentVersion::V1Only,
8642 None,
8643 dh,
8644 dm,
8645 config,
8646 test_dht_rx(),
8647 test_dht_rx(),
8648 None,
8649 None,
8650 crate::slot_tuner::SlotTuner::disabled(4),
8651 atx,
8652 amask,
8653 None,
8654 None,
8655 test_ban_manager(),
8656 test_ip_filter(),
8657 Arc::new(Vec::new()),
8658 None,
8659 None,
8660 Arc::new(crate::transport::NetworkFactory::tokio()),
8661 None, Arc::new(crate::stats::SessionCounters::new()),
8663 )
8664 .await
8665 .unwrap();
8666
8667 let mut saw_checking = false;
8669 let mut progress_values: Vec<f32> = Vec::new();
8670 let mut saw_checked = false;
8671 let mut checked_have = 0u32;
8672 let mut checked_total = 0u32;
8673
8674 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
8675 while tokio::time::Instant::now() < deadline {
8676 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
8677 Ok(Ok(alert)) => match alert.kind {
8678 AlertKind::StateChanged {
8679 new_state: TorrentState::Checking,
8680 ..
8681 } => {
8682 saw_checking = true;
8683 }
8684 AlertKind::CheckingProgress { progress, .. } => {
8685 progress_values.push(progress);
8686 }
8687 AlertKind::TorrentChecked {
8688 pieces_have,
8689 pieces_total,
8690 ..
8691 } => {
8692 saw_checked = true;
8693 checked_have = pieces_have;
8694 checked_total = pieces_total;
8695 break;
8696 }
8697 _ => {}
8698 },
8699 _ => break,
8700 }
8701 }
8702
8703 assert!(saw_checking, "should have seen StateChanged → Checking");
8704 assert!(
8705 !progress_values.is_empty(),
8706 "should have seen CheckingProgress alerts"
8707 );
8708 for w in progress_values.windows(2) {
8710 assert!(
8711 w[1] >= w[0],
8712 "progress should be monotonically increasing: {} < {}",
8713 w[0],
8714 w[1]
8715 );
8716 }
8717 assert!(saw_checked, "should have seen TorrentChecked");
8718 assert_eq!(checked_have, 4);
8719 assert_eq!(checked_total, 4);
8720
8721 tokio::time::sleep(Duration::from_millis(50)).await;
8723 let stats = handle.stats().await.unwrap();
8724 assert_eq!(stats.state, TorrentState::Seeding);
8725
8726 handle.shutdown().await.unwrap();
8727 }
8728
8729 #[tokio::test]
8730 #[allow(clippy::float_cmp, reason = "exact sentinel value comparison (0.0)")]
8731 async fn checking_progress_in_stats() {
8732 let data = vec![0xAB; 32768];
8734 let meta = make_test_torrent(&data, 16384);
8735 let storage = make_storage(&data, 16384);
8736 let config = test_config();
8737
8738 let (atx, amask) = test_alert_channel();
8739 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8740 let handle = TorrentHandle::from_torrent(
8741 meta,
8742 irontide_core::TorrentVersion::V1Only,
8743 None,
8744 dh,
8745 dm,
8746 config,
8747 test_dht_rx(),
8748 test_dht_rx(),
8749 None,
8750 None,
8751 crate::slot_tuner::SlotTuner::disabled(4),
8752 atx,
8753 amask,
8754 None,
8755 None,
8756 test_ban_manager(),
8757 test_ip_filter(),
8758 Arc::new(Vec::new()),
8759 None,
8760 None,
8761 Arc::new(crate::transport::NetworkFactory::tokio()),
8762 None, Arc::new(crate::stats::SessionCounters::new()),
8764 )
8765 .await
8766 .unwrap();
8767
8768 tokio::time::sleep(Duration::from_millis(100)).await;
8770
8771 let stats = handle.stats().await.unwrap();
8772 assert_eq!(stats.state, TorrentState::Downloading);
8773 assert_eq!(
8774 stats.checking_progress, 0.0,
8775 "checking_progress should be 0.0 when not checking"
8776 );
8777
8778 handle.shutdown().await.unwrap();
8779 }
8780
8781 #[tokio::test]
8782 async fn verify_pieces_partial_data() {
8783 use crate::alert::AlertKind;
8784
8785 let data = vec![0xCCu8; 65536]; let meta = make_test_torrent(&data, 16384);
8788
8789 let lengths = Lengths::new(data.len() as u64, 16384, DEFAULT_CHUNK_SIZE);
8791 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
8792 for p in 0..2u32 {
8793 let offset = lengths.piece_offset(p) as usize;
8794 let size = lengths.piece_size(p) as usize;
8795 storage
8796 .write_chunk(p, 0, &data[offset..offset + size])
8797 .unwrap();
8798 }
8799 let config = test_config();
8802 let (atx, amask) = test_alert_channel();
8803 let mut rx = atx.subscribe();
8804 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8805 let handle = TorrentHandle::from_torrent(
8806 meta,
8807 irontide_core::TorrentVersion::V1Only,
8808 None,
8809 dh,
8810 dm,
8811 config,
8812 test_dht_rx(),
8813 test_dht_rx(),
8814 None,
8815 None,
8816 crate::slot_tuner::SlotTuner::disabled(4),
8817 atx,
8818 amask,
8819 None,
8820 None,
8821 test_ban_manager(),
8822 test_ip_filter(),
8823 Arc::new(Vec::new()),
8824 None,
8825 None,
8826 Arc::new(crate::transport::NetworkFactory::tokio()),
8827 None, Arc::new(crate::stats::SessionCounters::new()),
8829 )
8830 .await
8831 .unwrap();
8832
8833 let mut checked_have = 0u32;
8835 let mut checked_total = 0u32;
8836 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
8837 while tokio::time::Instant::now() < deadline {
8838 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
8839 Ok(Ok(alert)) => {
8840 if let AlertKind::TorrentChecked {
8841 pieces_have,
8842 pieces_total,
8843 ..
8844 } = alert.kind
8845 {
8846 checked_have = pieces_have;
8847 checked_total = pieces_total;
8848 break;
8849 }
8850 }
8851 _ => break,
8852 }
8853 }
8854
8855 assert_eq!(checked_have, 2, "only 2 pieces should be valid");
8856 assert_eq!(checked_total, 4);
8857
8858 tokio::time::sleep(Duration::from_millis(50)).await;
8860 let stats = handle.stats().await.unwrap();
8861 assert_eq!(stats.state, TorrentState::Downloading);
8862 assert_eq!(stats.pieces_have, 2);
8863 assert_eq!(stats.pieces_total, 4);
8864
8865 handle.shutdown().await.unwrap();
8866 }
8867
8868 #[tokio::test]
8871 async fn ip_filter_blocks_peers_in_handle_add_peers() {
8872 let data = vec![0xCD; 32768];
8873 let meta = make_test_torrent(&data, 16384);
8874 let storage = make_storage(&data, 16384);
8875 let config = test_config();
8876
8877 let ip_filter = {
8879 let mut f = crate::ip_filter::IpFilter::new();
8880 f.add_rule(
8881 "203.0.113.0".parse().unwrap(),
8882 "203.0.113.255".parse().unwrap(),
8883 1,
8884 );
8885 Arc::new(parking_lot::RwLock::new(f))
8886 };
8887
8888 let (atx, amask) = test_alert_channel();
8889 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8890 let handle = TorrentHandle::from_torrent(
8891 meta,
8892 irontide_core::TorrentVersion::V1Only,
8893 None,
8894 dh,
8895 dm,
8896 config,
8897 test_dht_rx(),
8898 test_dht_rx(),
8899 None,
8900 None,
8901 crate::slot_tuner::SlotTuner::disabled(4),
8902 atx,
8903 amask,
8904 None,
8905 None,
8906 test_ban_manager(),
8907 Arc::clone(&ip_filter),
8908 Arc::new(Vec::new()),
8909 None,
8910 None,
8911 Arc::new(crate::transport::NetworkFactory::tokio()),
8912 None, Arc::new(crate::stats::SessionCounters::new()),
8914 )
8915 .await
8916 .unwrap();
8917
8918 let blocked_addr: SocketAddr = "203.0.113.42:6881".parse().unwrap();
8920 let allowed_addr: SocketAddr = "198.51.100.1:6881".parse().unwrap();
8921 handle
8922 .add_peers(vec![blocked_addr, allowed_addr], PeerSource::Tracker)
8923 .await
8924 .unwrap();
8925
8926 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
8927 let stats = handle.stats().await.unwrap();
8928 assert!(
8930 stats.peers_available + stats.peers_connected <= 1,
8931 "blocked peer should not be added: available={}, connected={}",
8932 stats.peers_available,
8933 stats.peers_connected
8934 );
8935
8936 handle.shutdown().await.unwrap();
8937 }
8938
8939 #[tokio::test]
8940 async fn set_ip_filter_replaces_filter_and_blocks_new_ip() {
8941 let data = vec![0xCD; 32768];
8944 let meta = make_test_torrent(&data, 16384);
8945 let storage = make_storage(&data, 16384);
8946 let config = test_config();
8947
8948 let ip_filter: crate::session::SharedIpFilter =
8950 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
8951
8952 let (atx, amask) = test_alert_channel();
8953 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8954 let handle = TorrentHandle::from_torrent(
8955 meta,
8956 irontide_core::TorrentVersion::V1Only,
8957 None,
8958 dh,
8959 dm,
8960 config,
8961 test_dht_rx(),
8962 test_dht_rx(),
8963 None,
8964 None,
8965 crate::slot_tuner::SlotTuner::disabled(4),
8966 atx,
8967 amask,
8968 None,
8969 None,
8970 test_ban_manager(),
8971 Arc::clone(&ip_filter),
8972 Arc::new(Vec::new()),
8973 None,
8974 None,
8975 Arc::new(crate::transport::NetworkFactory::tokio()),
8976 None, Arc::new(crate::stats::SessionCounters::new()),
8978 )
8979 .await
8980 .unwrap();
8981
8982 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8985 let local_addr = listener.local_addr().unwrap();
8986 handle
8987 .add_peers(vec![local_addr], PeerSource::Tracker)
8988 .await
8989 .unwrap();
8990 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
8991 let stats = handle.stats().await.unwrap();
8992 assert!(
8993 stats.peers_available + stats.peers_connected >= 1,
8994 "peer should be allowed initially"
8995 );
8996 handle.shutdown().await.unwrap();
8997
8998 {
9000 let mut f = ip_filter.write();
9001 f.add_rule(
9002 "198.51.100.0".parse().unwrap(),
9003 "198.51.100.255".parse().unwrap(),
9004 1,
9005 );
9006 }
9007
9008 assert!(ip_filter.read().is_blocked("198.51.100.1".parse().unwrap()));
9010 assert!(!ip_filter.read().is_blocked("203.0.113.1".parse().unwrap()));
9012 }
9013
9014 #[test]
9015 fn relocate_files_moves_and_cleans_up() {
9016 let tmp = std::env::temp_dir().join(format!("torrent_relocate_{}", std::process::id()));
9017 let src = tmp.join("src");
9018 let dst = tmp.join("dst");
9019
9020 let subdir = src.join("TorrentName").join("subdir");
9024 std::fs::create_dir_all(&subdir).unwrap();
9025 std::fs::write(subdir.join("file1.txt"), b"hello").unwrap();
9026 std::fs::write(src.join("TorrentName").join("file2.txt"), b"world").unwrap();
9027
9028 let file_paths = vec![
9029 std::path::PathBuf::from("TorrentName/subdir/file1.txt"),
9030 std::path::PathBuf::from("TorrentName/file2.txt"),
9031 ];
9032
9033 relocate_files(&src, &dst, &file_paths).unwrap();
9034
9035 assert_eq!(
9037 std::fs::read_to_string(dst.join("TorrentName/subdir/file1.txt")).unwrap(),
9038 "hello"
9039 );
9040 assert_eq!(
9041 std::fs::read_to_string(dst.join("TorrentName/file2.txt")).unwrap(),
9042 "world"
9043 );
9044
9045 assert!(!src.join("TorrentName").join("subdir").exists());
9047 assert!(!src.join("TorrentName").exists());
9048
9049 let _ = std::fs::remove_dir_all(&tmp);
9051 }
9052
9053 #[test]
9054 fn relocate_files_skips_missing() {
9055 let tmp =
9056 std::env::temp_dir().join(format!("torrent_relocate_skip_{}", std::process::id()));
9057 let src = tmp.join("src");
9058 let dst = tmp.join("dst");
9059 std::fs::create_dir_all(&src).unwrap();
9060
9061 let file_paths = vec![std::path::PathBuf::from("nonexistent.txt")];
9063 relocate_files(&src, &dst, &file_paths).unwrap();
9064
9065 assert!(!dst.join("nonexistent.txt").exists());
9066
9067 let _ = std::fs::remove_dir_all(&tmp);
9068 }
9069
9070 #[tokio::test]
9073 async fn force_recheck_transitions_to_checking() {
9074 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
9076 let storage = make_seeded_storage(&data, 16384);
9077 let config = test_config();
9078
9079 let (atx, amask) = test_alert_channel();
9080 let mut arx = atx.subscribe();
9081 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9082 let handle = TorrentHandle::from_torrent(
9083 meta,
9084 irontide_core::TorrentVersion::V1Only,
9085 None,
9086 dh,
9087 dm,
9088 config,
9089 test_dht_rx(),
9090 test_dht_rx(),
9091 None,
9092 None,
9093 crate::slot_tuner::SlotTuner::disabled(4),
9094 atx,
9095 amask,
9096 None,
9097 None,
9098 test_ban_manager(),
9099 test_ip_filter(),
9100 Arc::new(Vec::new()),
9101 None,
9102 None,
9103 Arc::new(crate::transport::NetworkFactory::tokio()),
9104 None, Arc::new(crate::stats::SessionCounters::new()),
9106 )
9107 .await
9108 .unwrap();
9109
9110 tokio::time::sleep(Duration::from_millis(100)).await;
9112 let stats = handle.stats().await.unwrap();
9113 assert_eq!(stats.state, TorrentState::Seeding, "should start as seeder");
9114
9115 while arx.try_recv().is_ok() {}
9117
9118 handle.force_recheck().await.unwrap();
9120
9121 let mut saw_checking = false;
9124 while let Ok(alert) = arx.try_recv() {
9125 if let crate::alert::AlertKind::StateChanged { new_state, .. } = alert.kind
9126 && new_state == TorrentState::Checking
9127 {
9128 saw_checking = true;
9129 }
9130 }
9131 assert!(
9132 saw_checking,
9133 "should have transitioned through Checking state"
9134 );
9135
9136 handle.shutdown().await.unwrap();
9137 }
9138
9139 #[tokio::test]
9142 async fn force_recheck_completes() {
9143 let data = vec![0xEEu8; 32768]; let meta = make_test_torrent(&data, 16384);
9145 let storage = make_seeded_storage(&data, 16384);
9146 let config = test_config();
9147
9148 let (atx, amask) = test_alert_channel();
9149 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9150 let handle = TorrentHandle::from_torrent(
9151 meta,
9152 irontide_core::TorrentVersion::V1Only,
9153 None,
9154 dh,
9155 dm,
9156 config,
9157 test_dht_rx(),
9158 test_dht_rx(),
9159 None,
9160 None,
9161 crate::slot_tuner::SlotTuner::disabled(4),
9162 atx,
9163 amask,
9164 None,
9165 None,
9166 test_ban_manager(),
9167 test_ip_filter(),
9168 Arc::new(Vec::new()),
9169 None,
9170 None,
9171 Arc::new(crate::transport::NetworkFactory::tokio()),
9172 None, Arc::new(crate::stats::SessionCounters::new()),
9174 )
9175 .await
9176 .unwrap();
9177
9178 tokio::time::sleep(Duration::from_millis(100)).await;
9180 let stats = handle.stats().await.unwrap();
9181 assert_eq!(stats.state, TorrentState::Seeding);
9182 assert_eq!(stats.pieces_have, 2);
9183
9184 handle.force_recheck().await.unwrap();
9186
9187 let stats = handle.stats().await.unwrap();
9188 assert_eq!(
9189 stats.state,
9190 TorrentState::Seeding,
9191 "should return to Seeding after recheck"
9192 );
9193 assert_eq!(stats.pieces_have, 2, "all pieces should still be verified");
9194
9195 handle.shutdown().await.unwrap();
9196 }
9197
9198 #[tokio::test]
9201 async fn rename_file_succeeds() {
9202 let tmp = std::env::temp_dir().join(format!("torrent_rename_{}", std::process::id()));
9204 std::fs::create_dir_all(&tmp).unwrap();
9205
9206 let data = vec![0xFFu8; 16384]; let meta = make_test_torrent(&data, 16384);
9208 let storage = make_seeded_storage(&data, 16384);
9209
9210 std::fs::write(tmp.join("test"), &data).unwrap();
9213
9214 let mut config = test_config();
9215 config.download_dir = tmp.clone();
9216
9217 let (atx, amask) = test_alert_channel();
9218 let mut arx = atx.subscribe();
9219 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9220 let handle = TorrentHandle::from_torrent(
9221 meta,
9222 irontide_core::TorrentVersion::V1Only,
9223 None,
9224 dh,
9225 dm,
9226 config,
9227 test_dht_rx(),
9228 test_dht_rx(),
9229 None,
9230 None,
9231 crate::slot_tuner::SlotTuner::disabled(4),
9232 atx,
9233 amask,
9234 None,
9235 None,
9236 test_ban_manager(),
9237 test_ip_filter(),
9238 Arc::new(Vec::new()),
9239 None,
9240 None,
9241 Arc::new(crate::transport::NetworkFactory::tokio()),
9242 None, Arc::new(crate::stats::SessionCounters::new()),
9244 )
9245 .await
9246 .unwrap();
9247
9248 tokio::time::sleep(Duration::from_millis(100)).await;
9250
9251 while arx.try_recv().is_ok() {}
9253
9254 handle.rename_file(0, "test_renamed".into()).await.unwrap();
9256
9257 assert!(!tmp.join("test").exists(), "old file should be removed");
9259 assert!(tmp.join("test_renamed").exists(), "new file should exist");
9260
9261 let mut saw_renamed = false;
9263 while let Ok(alert) = arx.try_recv() {
9264 if let AlertKind::FileRenamed { index, .. } = alert.kind {
9265 assert_eq!(index, 0);
9266 saw_renamed = true;
9267 }
9268 }
9269 assert!(saw_renamed, "should have received FileRenamed alert");
9270
9271 handle.shutdown().await.unwrap();
9272 let _ = std::fs::remove_dir_all(&tmp);
9273 }
9274
9275 #[tokio::test]
9278 async fn rename_file_invalid_index_errors() {
9279 let data = vec![0xCCu8; 16384]; let meta = make_test_torrent(&data, 16384);
9281 let storage = make_seeded_storage(&data, 16384);
9282 let config = test_config();
9283
9284 let (atx, amask) = test_alert_channel();
9285 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9286 let handle = TorrentHandle::from_torrent(
9287 meta,
9288 irontide_core::TorrentVersion::V1Only,
9289 None,
9290 dh,
9291 dm,
9292 config,
9293 test_dht_rx(),
9294 test_dht_rx(),
9295 None,
9296 None,
9297 crate::slot_tuner::SlotTuner::disabled(4),
9298 atx,
9299 amask,
9300 None,
9301 None,
9302 test_ban_manager(),
9303 test_ip_filter(),
9304 Arc::new(Vec::new()),
9305 None,
9306 None,
9307 Arc::new(crate::transport::NetworkFactory::tokio()),
9308 None, Arc::new(crate::stats::SessionCounters::new()),
9310 )
9311 .await
9312 .unwrap();
9313
9314 tokio::time::sleep(Duration::from_millis(100)).await;
9316
9317 let result = handle.rename_file(99, "bad".into()).await;
9319 assert!(result.is_err(), "should fail for out-of-range file index");
9320
9321 handle.shutdown().await.unwrap();
9322 }
9323
9324 #[tokio::test]
9327 async fn file_completed_alert_fires() {
9328 let data = vec![0xBBu8; 32768]; let meta = make_test_torrent(&data, 16384);
9330 let storage = make_seeded_storage(&data, 16384);
9331 let config = test_config();
9332
9333 let (atx, amask) = test_alert_channel();
9334 let mut arx = atx.subscribe();
9335 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9336 let handle = TorrentHandle::from_torrent(
9337 meta,
9338 irontide_core::TorrentVersion::V1Only,
9339 None,
9340 dh,
9341 dm,
9342 config,
9343 test_dht_rx(),
9344 test_dht_rx(),
9345 None,
9346 None,
9347 crate::slot_tuner::SlotTuner::disabled(4),
9348 atx,
9349 amask,
9350 None,
9351 None,
9352 test_ban_manager(),
9353 test_ip_filter(),
9354 Arc::new(Vec::new()),
9355 None,
9356 None,
9357 Arc::new(crate::transport::NetworkFactory::tokio()),
9358 None, Arc::new(crate::stats::SessionCounters::new()),
9360 )
9361 .await
9362 .unwrap();
9363
9364 tokio::time::sleep(Duration::from_millis(200)).await;
9366
9367 let mut saw_file_completed = false;
9369 while let Ok(alert) = arx.try_recv() {
9370 if let AlertKind::FileCompleted { file_index, .. } = alert.kind {
9371 assert_eq!(file_index, 0, "should be file index 0");
9372 saw_file_completed = true;
9373 }
9374 }
9375 assert!(
9376 saw_file_completed,
9377 "should have received FileCompleted alert"
9378 );
9379
9380 handle.shutdown().await.unwrap();
9381 }
9382
9383 #[test]
9386 fn metadata_failed_alert_fires() {
9387 let info_hash = Id20::from([0u8; 20]);
9389 let alert = crate::alert::Alert::new(AlertKind::MetadataFailed { info_hash });
9390 assert!(
9391 alert
9392 .category()
9393 .contains(crate::alert::AlertCategory::STATUS),
9394 "MetadataFailed should have STATUS category"
9395 );
9396 assert!(
9397 alert
9398 .category()
9399 .contains(crate::alert::AlertCategory::ERROR),
9400 "MetadataFailed should have ERROR category"
9401 );
9402
9403 let (tx, mut rx) = broadcast::channel(16);
9405 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
9406 post_alert(&tx, &mask, AlertKind::MetadataFailed { info_hash });
9407 let received = rx.try_recv().expect("should receive MetadataFailed alert");
9408 assert!(matches!(received.kind, AlertKind::MetadataFailed { .. }));
9409 }
9410
9411 #[tokio::test]
9414 async fn set_max_connections_persists() {
9415 let data = vec![0xAB; 32768];
9416 let meta = make_test_torrent(&data, 16384);
9417 let storage = make_storage(&data, 16384);
9418 let config = test_config();
9419
9420 let (atx, amask) = test_alert_channel();
9421 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9422 let handle = TorrentHandle::from_torrent(
9423 meta,
9424 irontide_core::TorrentVersion::V1Only,
9425 None,
9426 dh,
9427 dm,
9428 config,
9429 test_dht_rx(),
9430 test_dht_rx(),
9431 None,
9432 None,
9433 crate::slot_tuner::SlotTuner::disabled(4),
9434 atx,
9435 amask,
9436 None,
9437 None,
9438 test_ban_manager(),
9439 test_ip_filter(),
9440 Arc::new(Vec::new()),
9441 None,
9442 None,
9443 Arc::new(crate::transport::NetworkFactory::tokio()),
9444 None, Arc::new(crate::stats::SessionCounters::new()),
9446 )
9447 .await
9448 .unwrap();
9449
9450 handle.set_max_connections(10).await.unwrap();
9452 let val = handle.max_connections().await.unwrap();
9453 assert_eq!(val, 10);
9454
9455 handle.set_max_connections(25).await.unwrap();
9457 let val = handle.max_connections().await.unwrap();
9458 assert_eq!(val, 25);
9459
9460 let stats = handle.stats().await.unwrap();
9462 assert_eq!(stats.connections_limit, 25);
9463
9464 handle.shutdown().await.unwrap();
9465 }
9466
9467 #[tokio::test]
9470 async fn max_connections_default() {
9471 let data = vec![0xAB; 32768];
9472 let meta = make_test_torrent(&data, 16384);
9473 let storage = make_storage(&data, 16384);
9474 let config = test_config();
9475 let expected_default = config.max_peers;
9476
9477 let (atx, amask) = test_alert_channel();
9478 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9479 let handle = TorrentHandle::from_torrent(
9480 meta,
9481 irontide_core::TorrentVersion::V1Only,
9482 None,
9483 dh,
9484 dm,
9485 config,
9486 test_dht_rx(),
9487 test_dht_rx(),
9488 None,
9489 None,
9490 crate::slot_tuner::SlotTuner::disabled(4),
9491 atx,
9492 amask,
9493 None,
9494 None,
9495 test_ban_manager(),
9496 test_ip_filter(),
9497 Arc::new(Vec::new()),
9498 None,
9499 None,
9500 Arc::new(crate::transport::NetworkFactory::tokio()),
9501 None, Arc::new(crate::stats::SessionCounters::new()),
9503 )
9504 .await
9505 .unwrap();
9506
9507 let val = handle.max_connections().await.unwrap();
9509 assert_eq!(val, 0);
9510
9511 let stats = handle.stats().await.unwrap();
9513 assert_eq!(stats.connections_limit, expected_default);
9514
9515 handle.shutdown().await.unwrap();
9516 }
9517
9518 #[tokio::test]
9521 async fn set_max_uploads_round_trip() {
9522 let data = vec![0xAB; 32768];
9523 let meta = make_test_torrent(&data, 16384);
9524 let storage = make_storage(&data, 16384);
9525 let config = test_config();
9526
9527 let (atx, amask) = test_alert_channel();
9528 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9529 let handle = TorrentHandle::from_torrent(
9530 meta,
9531 irontide_core::TorrentVersion::V1Only,
9532 None,
9533 dh,
9534 dm,
9535 config,
9536 test_dht_rx(),
9537 test_dht_rx(),
9538 None,
9539 None,
9540 crate::slot_tuner::SlotTuner::disabled(4),
9541 atx,
9542 amask,
9543 None,
9544 None,
9545 test_ban_manager(),
9546 test_ip_filter(),
9547 Arc::new(Vec::new()),
9548 None,
9549 None,
9550 Arc::new(crate::transport::NetworkFactory::tokio()),
9551 None, Arc::new(crate::stats::SessionCounters::new()),
9553 )
9554 .await
9555 .unwrap();
9556
9557 handle.set_max_uploads(8).await.unwrap();
9559 let val = handle.max_uploads().await.unwrap();
9560 assert_eq!(val, 8);
9561
9562 let stats = handle.stats().await.unwrap();
9564 assert_eq!(stats.uploads_limit, 8);
9565
9566 handle.shutdown().await.unwrap();
9567 }
9568
9569 #[tokio::test]
9572 async fn external_ip_detected_alert() {
9573 let data = vec![0xAB; 32768];
9574 let meta = make_test_torrent(&data, 16384);
9575 let storage = make_storage(&data, 16384);
9576 let config = test_config();
9577
9578 let (atx, amask) = test_alert_channel();
9579 let mut arx = atx.subscribe();
9580 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9581 let handle = TorrentHandle::from_torrent(
9582 meta,
9583 irontide_core::TorrentVersion::V1Only,
9584 None,
9585 dh,
9586 dm,
9587 config,
9588 test_dht_rx(),
9589 test_dht_rx(),
9590 None,
9591 None,
9592 crate::slot_tuner::SlotTuner::disabled(4),
9593 atx,
9594 amask,
9595 None,
9596 None,
9597 test_ban_manager(),
9598 test_ip_filter(),
9599 Arc::new(Vec::new()),
9600 None,
9601 None,
9602 Arc::new(crate::transport::NetworkFactory::tokio()),
9603 None, Arc::new(crate::stats::SessionCounters::new()),
9605 )
9606 .await
9607 .unwrap();
9608
9609 while arx.try_recv().is_ok() {}
9611
9612 let test_ip: std::net::IpAddr = "203.0.113.42".parse().unwrap();
9614 handle
9615 .cmd_tx
9616 .send(TorrentCommand::UpdateExternalIp { ip: test_ip })
9617 .await
9618 .unwrap();
9619
9620 tokio::time::sleep(Duration::from_millis(50)).await;
9622
9623 let mut saw_alert = false;
9625 while let Ok(alert) = arx.try_recv() {
9626 if let AlertKind::ExternalIpDetected { ip } = alert.kind {
9627 assert_eq!(ip, test_ip);
9628 saw_alert = true;
9629 }
9630 }
9631 assert!(saw_alert, "should have received ExternalIpDetected alert");
9632
9633 handle.shutdown().await.unwrap();
9634 }
9635
9636 #[tokio::test]
9639 async fn get_peer_info_returns_connected_peers() {
9640 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
9643 let config = test_config();
9644
9645 let (atx, amask) = test_alert_channel();
9646 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9647 let handle = TorrentHandle::from_torrent(
9648 meta.clone(),
9649 irontide_core::TorrentVersion::V1Only,
9650 None,
9651 dh,
9652 dm,
9653 config,
9654 test_dht_rx(),
9655 test_dht_rx(),
9656 None,
9657 None,
9658 crate::slot_tuner::SlotTuner::disabled(4),
9659 atx,
9660 amask,
9661 None,
9662 None,
9663 test_ban_manager(),
9664 test_ip_filter(),
9665 Arc::new(Vec::new()),
9666 None,
9667 None,
9668 Arc::new(crate::transport::NetworkFactory::tokio()),
9669 None, Arc::new(crate::stats::SessionCounters::new()),
9671 )
9672 .await
9673 .unwrap();
9674
9675 let stats = handle.stats().await.unwrap();
9677 let listen_port = stats.peers_connected; let peer_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9681 let peer_addr = peer_listener.local_addr().unwrap();
9682
9683 handle
9684 .add_peers(vec![peer_addr], PeerSource::Tracker)
9685 .await
9686 .unwrap();
9687
9688 let accept_timeout =
9690 tokio::time::timeout(Duration::from_secs(2), peer_listener.accept()).await;
9691 if let Ok(Ok((mut stream, _))) = accept_timeout {
9692 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
9694 if tokio::time::timeout(Duration::from_millis(500), stream.read_exact(&mut hs_buf))
9695 .await
9696 .is_ok()
9697 {
9698 let hs = Handshake::new(meta.info_hash, Id20::from([0xBB; 20]));
9700 let hs_bytes = hs.to_bytes();
9701 let _ = stream.write_all(&hs_bytes).await;
9702
9703 tokio::time::sleep(Duration::from_millis(200)).await;
9705
9706 let peer_info = handle.get_peer_info().await.unwrap();
9708 if !peer_info.is_empty() {
9710 let p = &peer_info[0];
9711 assert!(p.peer_choking, "peer should be choking us initially");
9713 assert!(
9715 !p.am_choking,
9716 "we should not be choking peer after connect (M107 unconditional unchoke)"
9717 );
9718 assert!(
9719 !p.peer_interested,
9720 "peer should not be interested initially"
9721 );
9722 assert_eq!(p.num_pieces, 0);
9723 assert_eq!(p.source, PeerSource::Tracker);
9724 }
9725 }
9726 }
9727 let _ = handle.get_peer_info().await.unwrap();
9729 assert_eq!(listen_port, 0); handle.shutdown().await.unwrap();
9732 }
9733
9734 #[tokio::test]
9737 async fn get_peer_info_empty_when_no_peers() {
9738 let data = vec![0xAB; 32768];
9739 let meta = make_test_torrent(&data, 16384);
9740 let storage = make_storage(&data, 16384);
9741 let config = test_config();
9742
9743 let (atx, amask) = test_alert_channel();
9744 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9745 let handle = TorrentHandle::from_torrent(
9746 meta,
9747 irontide_core::TorrentVersion::V1Only,
9748 None,
9749 dh,
9750 dm,
9751 config,
9752 test_dht_rx(),
9753 test_dht_rx(),
9754 None,
9755 None,
9756 crate::slot_tuner::SlotTuner::disabled(4),
9757 atx,
9758 amask,
9759 None,
9760 None,
9761 test_ban_manager(),
9762 test_ip_filter(),
9763 Arc::new(Vec::new()),
9764 None,
9765 None,
9766 Arc::new(crate::transport::NetworkFactory::tokio()),
9767 None, Arc::new(crate::stats::SessionCounters::new()),
9769 )
9770 .await
9771 .unwrap();
9772
9773 let peer_info = handle.get_peer_info().await.unwrap();
9774 assert!(peer_info.is_empty(), "should have no peers initially");
9775
9776 handle.shutdown().await.unwrap();
9777 }
9778
9779 #[tokio::test]
9782 async fn get_download_queue_empty_initially() {
9783 let data = vec![0xAB; 32768];
9784 let meta = make_test_torrent(&data, 16384);
9785 let storage = make_storage(&data, 16384);
9786 let config = test_config();
9787
9788 let (atx, amask) = test_alert_channel();
9789 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9790 let handle = TorrentHandle::from_torrent(
9791 meta,
9792 irontide_core::TorrentVersion::V1Only,
9793 None,
9794 dh,
9795 dm,
9796 config,
9797 test_dht_rx(),
9798 test_dht_rx(),
9799 None,
9800 None,
9801 crate::slot_tuner::SlotTuner::disabled(4),
9802 atx,
9803 amask,
9804 None,
9805 None,
9806 test_ban_manager(),
9807 test_ip_filter(),
9808 Arc::new(Vec::new()),
9809 None,
9810 None,
9811 Arc::new(crate::transport::NetworkFactory::tokio()),
9812 None, Arc::new(crate::stats::SessionCounters::new()),
9814 )
9815 .await
9816 .unwrap();
9817
9818 let queue = handle.get_download_queue().await.unwrap();
9819 assert!(
9820 queue.is_empty(),
9821 "download queue should be empty with no active downloads"
9822 );
9823
9824 handle.shutdown().await.unwrap();
9825 }
9826
9827 #[tokio::test]
9830 async fn have_piece_false_initially() {
9831 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
9833 let storage = make_storage(&data, 16384);
9834 let config = test_config();
9835
9836 let (atx, amask) = test_alert_channel();
9837 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9838 let handle = TorrentHandle::from_torrent(
9839 meta,
9840 irontide_core::TorrentVersion::V1Only,
9841 None,
9842 dh,
9843 dm,
9844 config,
9845 test_dht_rx(),
9846 test_dht_rx(),
9847 None,
9848 None,
9849 crate::slot_tuner::SlotTuner::disabled(4),
9850 atx,
9851 amask,
9852 None,
9853 None,
9854 test_ban_manager(),
9855 test_ip_filter(),
9856 Arc::new(Vec::new()),
9857 None,
9858 None,
9859 Arc::new(crate::transport::NetworkFactory::tokio()),
9860 None, Arc::new(crate::stats::SessionCounters::new()),
9862 )
9863 .await
9864 .unwrap();
9865
9866 assert!(
9867 !handle.have_piece(0).await.unwrap(),
9868 "piece 0 should not be downloaded initially"
9869 );
9870 assert!(
9871 !handle.have_piece(1).await.unwrap(),
9872 "piece 1 should not be downloaded initially"
9873 );
9874
9875 handle.shutdown().await.unwrap();
9876 }
9877
9878 #[tokio::test]
9881 async fn piece_availability_empty_no_peers() {
9882 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
9884 let storage = make_storage(&data, 16384);
9885 let config = test_config();
9886
9887 let (atx, amask) = test_alert_channel();
9888 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9889 let handle = TorrentHandle::from_torrent(
9890 meta,
9891 irontide_core::TorrentVersion::V1Only,
9892 None,
9893 dh,
9894 dm,
9895 config,
9896 test_dht_rx(),
9897 test_dht_rx(),
9898 None,
9899 None,
9900 crate::slot_tuner::SlotTuner::disabled(4),
9901 atx,
9902 amask,
9903 None,
9904 None,
9905 test_ban_manager(),
9906 test_ip_filter(),
9907 Arc::new(Vec::new()),
9908 None,
9909 None,
9910 Arc::new(crate::transport::NetworkFactory::tokio()),
9911 None, Arc::new(crate::stats::SessionCounters::new()),
9913 )
9914 .await
9915 .unwrap();
9916
9917 let avail = handle.piece_availability().await.unwrap();
9918 assert_eq!(avail.len(), 2, "should have availability for 2 pieces");
9919 assert!(
9920 avail.iter().all(|&c| c == 0),
9921 "all availability counts should be 0 with no peers"
9922 );
9923
9924 handle.shutdown().await.unwrap();
9925 }
9926
9927 #[tokio::test]
9930 async fn file_progress_zeros_initially() {
9931 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
9933 let storage = make_storage(&data, 16384);
9934 let config = test_config();
9935
9936 let (atx, amask) = test_alert_channel();
9937 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9938 let handle = TorrentHandle::from_torrent(
9939 meta,
9940 irontide_core::TorrentVersion::V1Only,
9941 None,
9942 dh,
9943 dm,
9944 config,
9945 test_dht_rx(),
9946 test_dht_rx(),
9947 None,
9948 None,
9949 crate::slot_tuner::SlotTuner::disabled(4),
9950 atx,
9951 amask,
9952 None,
9953 None,
9954 test_ban_manager(),
9955 test_ip_filter(),
9956 Arc::new(Vec::new()),
9957 None,
9958 None,
9959 Arc::new(crate::transport::NetworkFactory::tokio()),
9960 None, Arc::new(crate::stats::SessionCounters::new()),
9962 )
9963 .await
9964 .unwrap();
9965
9966 let progress = handle.file_progress().await.unwrap();
9967 assert_eq!(progress.len(), 1, "single-file torrent should have 1 entry");
9968 assert_eq!(progress[0], 0, "no bytes should be downloaded initially");
9969
9970 handle.shutdown().await.unwrap();
9971 }
9972
9973 fn make_test_torrent_multi(
9977 data: &[u8],
9978 piece_length: u64,
9979 file_lengths: &[u64],
9980 ) -> TorrentMetaV1 {
9981 use serde::Serialize;
9982
9983 #[derive(Serialize)]
9984 struct FileE {
9985 length: u64,
9986 path: Vec<String>,
9987 }
9988
9989 #[derive(Serialize)]
9990 struct Info<'a> {
9991 name: &'a str,
9992 #[serde(rename = "piece length")]
9993 piece_length: u64,
9994 #[serde(with = "serde_bytes")]
9995 pieces: &'a [u8],
9996 files: Vec<FileE>,
9997 }
9998
9999 #[derive(Serialize)]
10000 struct Torrent<'a> {
10001 info: Info<'a>,
10002 }
10003
10004 let mut pieces = Vec::new();
10005 let mut offset = 0;
10006 while offset < data.len() {
10007 let end = (offset + piece_length as usize).min(data.len());
10008 let hash = irontide_core::sha1(&data[offset..end]);
10009 pieces.extend_from_slice(hash.as_bytes());
10010 offset = end;
10011 }
10012
10013 let files: Vec<FileE> = file_lengths
10014 .iter()
10015 .enumerate()
10016 .map(|(i, &len)| FileE {
10017 length: len,
10018 path: vec![format!("file{i}.bin")],
10019 })
10020 .collect();
10021
10022 let t = Torrent {
10023 info: Info {
10024 name: "test_multi",
10025 piece_length,
10026 pieces: &pieces,
10027 files,
10028 },
10029 };
10030
10031 let bytes = irontide_bencode::to_bytes(&t).unwrap();
10032 torrent_from_bytes(&bytes).unwrap()
10033 }
10034
10035 #[tokio::test]
10036 async fn file_progress_length_matches_file_count() {
10037 let data = vec![0xCD; 32768];
10039 let file_lengths = [10000u64, 20000, 2768];
10040 let meta = make_test_torrent_multi(&data, 16384, &file_lengths);
10041 let storage = make_storage(&data, 16384);
10042 let config = test_config();
10043
10044 let (atx, amask) = test_alert_channel();
10045 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10046 let handle = TorrentHandle::from_torrent(
10047 meta,
10048 irontide_core::TorrentVersion::V1Only,
10049 None,
10050 dh,
10051 dm,
10052 config,
10053 test_dht_rx(),
10054 test_dht_rx(),
10055 None,
10056 None,
10057 crate::slot_tuner::SlotTuner::disabled(4),
10058 atx,
10059 amask,
10060 None,
10061 None,
10062 test_ban_manager(),
10063 test_ip_filter(),
10064 Arc::new(Vec::new()),
10065 None,
10066 None,
10067 Arc::new(crate::transport::NetworkFactory::tokio()),
10068 None, Arc::new(crate::stats::SessionCounters::new()),
10070 )
10071 .await
10072 .unwrap();
10073
10074 let progress = handle.file_progress().await.unwrap();
10075 assert_eq!(
10076 progress.len(),
10077 3,
10078 "multi-file torrent should have 3 entries"
10079 );
10080 assert!(
10081 progress.iter().all(|&b| b == 0),
10082 "all progress should be 0 initially"
10083 );
10084
10085 handle.shutdown().await.unwrap();
10086 }
10087
10088 #[tokio::test]
10091 async fn is_valid_true_for_active() {
10092 let data = vec![0xAB; 32768];
10093 let meta = make_test_torrent(&data, 16384);
10094 let storage = make_storage(&data, 16384);
10095 let config = test_config();
10096
10097 let (atx, amask) = test_alert_channel();
10098 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10099 let handle = TorrentHandle::from_torrent(
10100 meta,
10101 irontide_core::TorrentVersion::V1Only,
10102 None,
10103 dh,
10104 dm,
10105 config,
10106 test_dht_rx(),
10107 test_dht_rx(),
10108 None,
10109 None,
10110 crate::slot_tuner::SlotTuner::disabled(4),
10111 atx,
10112 amask,
10113 None,
10114 None,
10115 test_ban_manager(),
10116 test_ip_filter(),
10117 Arc::new(Vec::new()),
10118 None,
10119 None,
10120 Arc::new(crate::transport::NetworkFactory::tokio()),
10121 None, Arc::new(crate::stats::SessionCounters::new()),
10123 )
10124 .await
10125 .unwrap();
10126
10127 assert!(
10128 handle.is_valid(),
10129 "handle should be valid while torrent actor is alive"
10130 );
10131
10132 handle.shutdown().await.unwrap();
10133 }
10134
10135 #[tokio::test]
10138 async fn is_valid_false_after_remove() {
10139 let data = vec![0xAB; 32768];
10140 let meta = make_test_torrent(&data, 16384);
10141 let storage = make_storage(&data, 16384);
10142 let config = test_config();
10143
10144 let (atx, amask) = test_alert_channel();
10145 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10146 let handle = TorrentHandle::from_torrent(
10147 meta,
10148 irontide_core::TorrentVersion::V1Only,
10149 None,
10150 dh,
10151 dm,
10152 config,
10153 test_dht_rx(),
10154 test_dht_rx(),
10155 None,
10156 None,
10157 crate::slot_tuner::SlotTuner::disabled(4),
10158 atx,
10159 amask,
10160 None,
10161 None,
10162 test_ban_manager(),
10163 test_ip_filter(),
10164 Arc::new(Vec::new()),
10165 None,
10166 None,
10167 Arc::new(crate::transport::NetworkFactory::tokio()),
10168 None, Arc::new(crate::stats::SessionCounters::new()),
10170 )
10171 .await
10172 .unwrap();
10173
10174 assert!(handle.is_valid());
10175
10176 handle.shutdown().await.unwrap();
10178
10179 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
10181
10182 assert!(
10183 !handle.is_valid(),
10184 "handle should be invalid after shutdown"
10185 );
10186 }
10187
10188 #[tokio::test]
10191 async fn clear_error_resets() {
10192 let data = vec![0xAB; 32768];
10193 let meta = make_test_torrent(&data, 16384);
10194 let storage = make_storage(&data, 16384);
10195 let config = test_config();
10196
10197 let (atx, amask) = test_alert_channel();
10198 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10199 let handle = TorrentHandle::from_torrent(
10200 meta,
10201 irontide_core::TorrentVersion::V1Only,
10202 None,
10203 dh,
10204 dm,
10205 config,
10206 test_dht_rx(),
10207 test_dht_rx(),
10208 None,
10209 None,
10210 crate::slot_tuner::SlotTuner::disabled(4),
10211 atx,
10212 amask,
10213 None,
10214 None,
10215 test_ban_manager(),
10216 test_ip_filter(),
10217 Arc::new(Vec::new()),
10218 None,
10219 None,
10220 Arc::new(crate::transport::NetworkFactory::tokio()),
10221 None, Arc::new(crate::stats::SessionCounters::new()),
10223 )
10224 .await
10225 .unwrap();
10226
10227 let stats = handle.stats().await.unwrap();
10229 assert!(stats.error.is_empty());
10230 assert_eq!(stats.error_file, -1);
10231
10232 handle.clear_error().await.unwrap();
10234
10235 let stats = handle.stats().await.unwrap();
10236 assert!(stats.error.is_empty());
10237 assert_eq!(stats.error_file, -1);
10238
10239 handle.shutdown().await.unwrap();
10240 }
10241
10242 #[tokio::test]
10245 async fn flags_round_trip() {
10246 let data = vec![0xAB; 32768];
10247 let meta = make_test_torrent(&data, 16384);
10248 let storage = make_storage(&data, 16384);
10249 let config = test_config();
10250
10251 let (atx, amask) = test_alert_channel();
10252 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10253 let handle = TorrentHandle::from_torrent(
10254 meta,
10255 irontide_core::TorrentVersion::V1Only,
10256 None,
10257 dh,
10258 dm,
10259 config,
10260 test_dht_rx(),
10261 test_dht_rx(),
10262 None,
10263 None,
10264 crate::slot_tuner::SlotTuner::disabled(4),
10265 atx,
10266 amask,
10267 None,
10268 None,
10269 test_ban_manager(),
10270 test_ip_filter(),
10271 Arc::new(Vec::new()),
10272 None,
10273 None,
10274 Arc::new(crate::transport::NetworkFactory::tokio()),
10275 None, Arc::new(crate::stats::SessionCounters::new()),
10277 )
10278 .await
10279 .unwrap();
10280
10281 let initial = handle.flags().await.unwrap();
10283 assert!(!initial.contains(crate::types::TorrentFlags::PAUSED));
10284 assert!(!initial.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10285 assert!(!initial.contains(crate::types::TorrentFlags::SUPER_SEEDING));
10286
10287 handle
10289 .set_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
10290 .await
10291 .unwrap();
10292 let after_set = handle.flags().await.unwrap();
10293 assert!(after_set.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10294
10295 handle
10297 .unset_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
10298 .await
10299 .unwrap();
10300 let after_unset = handle.flags().await.unwrap();
10301 assert!(!after_unset.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
10302
10303 assert!(!handle.is_sequential_download().await.unwrap());
10305
10306 handle.shutdown().await.unwrap();
10307 }
10308
10309 #[tokio::test]
10312 async fn connect_peer_no_error() {
10313 let data = vec![0xAB; 32768];
10314 let meta = make_test_torrent(&data, 16384);
10315 let storage = make_storage(&data, 16384);
10316 let config = test_config();
10317
10318 let (atx, amask) = test_alert_channel();
10319 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10320 let handle = TorrentHandle::from_torrent(
10321 meta,
10322 irontide_core::TorrentVersion::V1Only,
10323 None,
10324 dh,
10325 dm,
10326 config,
10327 test_dht_rx(),
10328 test_dht_rx(),
10329 None,
10330 None,
10331 crate::slot_tuner::SlotTuner::disabled(4),
10332 atx,
10333 amask,
10334 None,
10335 None,
10336 test_ban_manager(),
10337 test_ip_filter(),
10338 Arc::new(Vec::new()),
10339 None,
10340 None,
10341 Arc::new(crate::transport::NetworkFactory::tokio()),
10342 None, Arc::new(crate::stats::SessionCounters::new()),
10344 )
10345 .await
10346 .unwrap();
10347
10348 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
10351 handle.connect_peer(addr).await.unwrap();
10352
10353 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
10355
10356 handle.shutdown().await.unwrap();
10357 }
10358
10359 fn make_test_meta_v2(
10363 piece_hashes: &[irontide_core::Id32],
10364 file_root: irontide_core::Id32,
10365 piece_length: u64,
10366 file_length: u64,
10367 ) -> irontide_core::TorrentMetaV2 {
10368 use std::collections::BTreeMap;
10369
10370 let mut layer_bytes = Vec::with_capacity(piece_hashes.len() * 32);
10372 for h in piece_hashes {
10373 layer_bytes.extend_from_slice(&h.0);
10374 }
10375
10376 let mut piece_layers = BTreeMap::new();
10377 piece_layers.insert(file_root, layer_bytes);
10378
10379 let file_tree = irontide_core::FileTreeNode::Directory({
10380 let mut children = BTreeMap::new();
10381 children.insert(
10382 "test.dat".to_string(),
10383 irontide_core::FileTreeNode::File(irontide_core::V2FileAttr {
10384 length: file_length,
10385 pieces_root: Some(file_root),
10386 }),
10387 );
10388 children
10389 });
10390
10391 irontide_core::TorrentMetaV2 {
10392 info_hashes: irontide_core::InfoHashes::v2_only(irontide_core::Id32::ZERO),
10393 info_bytes: None,
10394 announce: None,
10395 announce_list: None,
10396 comment: None,
10397 created_by: None,
10398 creation_date: None,
10399 info: irontide_core::InfoDictV2 {
10400 name: "test".to_string(),
10401 piece_length,
10402 meta_version: 2,
10403 file_tree,
10404 ssl_cert: None,
10405 },
10406 piece_layers,
10407 ssl_cert: None,
10408 }
10409 }
10410
10411 #[test]
10412 fn test_serve_hashes_v2_piece_layer() {
10413 let hashes: Vec<irontide_core::Id32> = (0..4u8)
10416 .map(|i| {
10417 let mut h = [0u8; 32];
10418 h[0] = i;
10419 irontide_core::Id32(h)
10420 })
10421 .collect();
10422 let file_root = irontide_core::Id32([0xAA; 32]);
10423 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
10424 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
10425
10426 let request = irontide_core::HashRequest {
10427 file_root,
10428 base: 0, index: 0,
10430 count: 4,
10431 proof_layers: 0,
10432 };
10433
10434 let result = serve_hashes(
10435 Some(&meta),
10436 irontide_core::TorrentVersion::V2Only,
10437 Some(&lengths),
10438 &request,
10439 );
10440 let served = result.expect("should serve hashes");
10441 assert_eq!(served.len(), 4);
10442 for (i, h) in served.iter().enumerate() {
10443 assert_eq!(h.0[0], i as u8);
10444 }
10445 }
10446
10447 #[test]
10448 fn test_serve_hashes_rejects_v1_only() {
10449 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
10450 let file_root = irontide_core::Id32([0xAA; 32]);
10451 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
10452 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
10453
10454 let request = irontide_core::HashRequest {
10455 file_root,
10456 base: 0,
10457 index: 0,
10458 count: 1,
10459 proof_layers: 0,
10460 };
10461
10462 let result = serve_hashes(
10463 Some(&meta),
10464 irontide_core::TorrentVersion::V1Only,
10465 Some(&lengths),
10466 &request,
10467 );
10468 assert!(result.is_none(), "V1Only should reject hash requests");
10469 }
10470
10471 #[test]
10472 fn test_serve_hashes_rejects_unknown_root() {
10473 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
10474 let file_root = irontide_core::Id32([0xAA; 32]);
10475 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
10476 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
10477
10478 let unknown_root = irontide_core::Id32([0xFF; 32]);
10480 let request = irontide_core::HashRequest {
10481 file_root: unknown_root,
10482 base: 0,
10483 index: 0,
10484 count: 1,
10485 proof_layers: 0,
10486 };
10487
10488 let result = serve_hashes(
10489 Some(&meta),
10490 irontide_core::TorrentVersion::V2Only,
10491 Some(&lengths),
10492 &request,
10493 );
10494 assert!(result.is_none(), "unknown file_root should reject");
10495 }
10496
10497 #[test]
10498 fn test_serve_hashes_rejects_out_of_bounds() {
10499 let hashes: Vec<irontide_core::Id32> =
10501 (0..2u8).map(|i| irontide_core::Id32([i; 32])).collect();
10502 let file_root = irontide_core::Id32([0xAA; 32]);
10503 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 2);
10504 let lengths = Lengths::new(16384 * 2, 16384, DEFAULT_CHUNK_SIZE);
10505
10506 let request = irontide_core::HashRequest {
10508 file_root,
10509 base: 0,
10510 index: 5,
10511 count: 1,
10512 proof_layers: 0,
10513 };
10514
10515 let result = serve_hashes(
10516 Some(&meta),
10517 irontide_core::TorrentVersion::V2Only,
10518 Some(&lengths),
10519 &request,
10520 );
10521 assert!(result.is_none(), "out-of-bounds index should reject");
10522 }
10523
10524 #[test]
10525 fn test_serve_hashes_includes_proofs() {
10526 let hashes: Vec<irontide_core::Id32> =
10529 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
10530 let file_root = irontide_core::Id32([0xAA; 32]);
10531 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
10532 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
10533
10534 let request = irontide_core::HashRequest {
10536 file_root,
10537 base: 0,
10538 index: 0,
10539 count: 1,
10540 proof_layers: 1,
10541 };
10542
10543 let result = serve_hashes(
10544 Some(&meta),
10545 irontide_core::TorrentVersion::V2Only,
10546 Some(&lengths),
10547 &request,
10548 );
10549 let served = result.expect("should serve hashes with proofs");
10550 assert_eq!(served.len(), 2, "should have 1 data hash + 1 proof hash");
10552 assert_eq!(served[0], hashes[0]);
10554 assert_eq!(served[1], hashes[1]);
10556 }
10557
10558 #[test]
10559 fn test_serve_hashes_proof_with_batch() {
10560 let hashes: Vec<irontide_core::Id32> =
10575 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
10576 let file_root = irontide_core::Id32([0xAA; 32]);
10577 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
10578 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
10579
10580 let request = irontide_core::HashRequest {
10581 file_root,
10582 base: 0,
10583 index: 0,
10584 count: 2,
10585 proof_layers: 1,
10586 };
10587
10588 let result = serve_hashes(
10589 Some(&meta),
10590 irontide_core::TorrentVersion::V2Only,
10591 Some(&lengths),
10592 &request,
10593 );
10594 let served = result.expect("should serve hashes with batch proof");
10595 assert_eq!(served.len(), 3, "should have 2 data hashes + 1 uncle hash");
10597 assert_eq!(served[0], hashes[0]);
10599 assert_eq!(served[1], hashes[1]);
10600 let tree = irontide_core::MerkleTree::from_leaves(&hashes);
10603 let expected_uncle = tree.layer(1)[1]; assert_eq!(served[2], expected_uncle);
10605
10606 let sub_root = irontide_core::MerkleTree::root_from_hashes(&served[..2]);
10609 let uncle_hashes = &served[2..];
10610 let leaf_index = request.index as usize / 2; assert!(
10612 irontide_core::MerkleTree::verify_proof(
10613 tree.root(),
10614 sub_root,
10615 leaf_index,
10616 uncle_hashes
10617 ),
10618 "subtree proof should verify against tree root"
10619 );
10620 }
10621
10622 #[test]
10623 fn is_i2p_synthetic_addr_detects_240_range() {
10624 assert!(is_i2p_synthetic_addr(&"240.0.0.1:1".parse().unwrap()));
10625 assert!(is_i2p_synthetic_addr(
10626 &"255.255.255.255:65535".parse().unwrap()
10627 ));
10628 assert!(!is_i2p_synthetic_addr(&"192.168.1.1:6881".parse().unwrap()));
10629 assert!(!is_i2p_synthetic_addr(&"[::1]:6881".parse().unwrap()));
10630 }
10631
10632 #[test]
10633 fn v6_retry_delay_progression() {
10634 let expected_ms = [100, 200, 400, 800, 1600, 3200, 5000, 5000, 5000, 5000, 5000];
10636 for (count, &expected) in expected_ms.iter().enumerate() {
10637 let delay_ms = {
10638 let base_ms: u64 = 100;
10639 let max_ms: u64 = 5000;
10640 base_ms
10641 .saturating_mul(1u64.checked_shl(count as u32).unwrap_or(u64::MAX))
10642 .min(max_ms)
10643 };
10644 assert_eq!(
10645 delay_ms, expected,
10646 "count={count}: expected {expected}ms, got {delay_ms}ms"
10647 );
10648 }
10649 }
10650
10651 #[test]
10654 fn peer_backoff_exponential() {
10655 let expected_ms: Vec<u64> = vec![400, 800, 1600, 3200, 6400, 12800, 25600, 30000, 30000];
10658 for (i, &expected) in expected_ms.iter().enumerate() {
10659 let attempt = (i as u32) + 1; let delay_ms = 200u64.saturating_mul(1u64 << attempt.min(10)).min(30_000);
10661 assert_eq!(
10662 delay_ms, expected,
10663 "attempt={attempt}: expected {expected}ms, got {delay_ms}ms"
10664 );
10665 }
10666 }
10667
10668 #[test]
10669 fn peer_backoff_clears_on_data() {
10670 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
10673 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
10674
10675 assert!(!backoff.contains_key(&addr));
10677
10678 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
10680 let next = attempt.saturating_add(1);
10681 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
10682 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
10683 backoff.insert(addr, (earliest, next));
10684 assert_eq!(backoff.get(&addr).unwrap().1, 1);
10685
10686 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
10688 let next = attempt.saturating_add(1);
10689 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
10690 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
10691 backoff.insert(addr, (earliest, next));
10692 assert_eq!(backoff.get(&addr).unwrap().1, 2);
10693
10694 backoff.remove(&addr);
10696 assert!(!backoff.contains_key(&addr));
10697 }
10698
10699 #[test]
10700 fn backoff_prevents_hammering() {
10701 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
10703 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
10704
10705 let future = std::time::Instant::now() + Duration::from_secs(10);
10707 backoff.insert(addr, (future, 3));
10708
10709 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
10711 assert!(std::time::Instant::now() < next_attempt);
10712 }
10713
10714 let past = std::time::Instant::now() - Duration::from_secs(1);
10716 backoff.insert(addr, (past, 3));
10717 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
10718 assert!(std::time::Instant::now() >= next_attempt);
10719 }
10720 }
10721
10722 #[test]
10723 fn max_in_flight_formula_updated() {
10724 let formula = |connected: usize, num_pieces: u32| -> usize {
10726 let calculated = 512usize.max(connected.saturating_mul(4));
10727 calculated.min(num_pieces as usize / 2).max(512)
10728 };
10729
10730 assert_eq!(formula(10, 2000), 512);
10732
10733 assert_eq!(formula(200, 2000), 800);
10735
10736 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);
10748
10749 assert_eq!(formula(100, 0), 512);
10751 }
10752
10753 #[test]
10756 fn should_attempt_holepunch_reason_classification() {
10757 assert!(should_attempt_holepunch("connection refused"));
10759 assert!(should_attempt_holepunch("Connection refused"));
10760 assert!(should_attempt_holepunch("timed out"));
10761 assert!(should_attempt_holepunch("Connection reset by peer"));
10762 assert!(should_attempt_holepunch("connection reset by peer"));
10763 assert!(!should_attempt_holepunch(
10765 "holepunch TCP connect failed: Connection refused"
10766 ));
10767 assert!(!should_attempt_holepunch("peer banned"));
10769 assert!(!should_attempt_holepunch("protocol error"));
10770 assert!(!should_attempt_holepunch(""));
10771 }
10772
10773 #[test]
10774 fn holepunch_initiation_on_connect_failure() {
10775 assert!(should_attempt_holepunch("connection refused"));
10777 }
10778
10779 #[test]
10780 fn holepunch_cooldown_prevents_retry() {
10781 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
10782 let addr: SocketAddr = "127.0.0.1:6881".parse().expect("valid test addr");
10783 let now = Instant::now();
10784 cooldowns.insert(addr, now);
10785 assert!(cooldowns.contains_key(&addr));
10787 }
10788
10789 #[test]
10790 fn holepunch_cooldown_overflow_skips() {
10791 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
10792 let now = Instant::now();
10793 for i in 0..256u16 {
10794 let addr: SocketAddr = format!("10.0.{}.{}:6881", i / 256, i % 256)
10795 .parse()
10796 .expect("valid test addr");
10797 cooldowns.insert(addr, now);
10798 }
10799 assert_eq!(cooldowns.len(), HOLEPUNCH_MAX_TRACKED);
10800 }
10802
10803 #[test]
10804 fn holepunch_skipped_when_disabled() {
10805 assert!(should_attempt_holepunch("connection refused"));
10808 }
10810
10811 #[test]
10812 fn holepunch_not_triggered_on_ban() {
10813 assert!(!should_attempt_holepunch("peer banned"));
10814 assert!(!should_attempt_holepunch("banned for bad data"));
10815 }
10816
10817 fn make_multi_file_meta(files: &[(u64, &str)], piece_length: u64) -> TorrentMetaV1 {
10821 let total_length: u64 = files.iter().map(|(len, _)| *len).sum();
10822 let num_pieces = total_length.div_ceil(piece_length) as usize;
10823 let file_entries: Vec<irontide_core::FileEntry> = files
10824 .iter()
10825 .map(|(length, name)| irontide_core::FileEntry {
10826 length: *length,
10827 path: vec![name.to_string()],
10828 attr: None,
10829 mtime: None,
10830 symlink_path: None,
10831 })
10832 .collect();
10833 TorrentMetaV1 {
10834 info_hash: Id20([0u8; 20]),
10835 announce: None,
10836 announce_list: None,
10837 comment: None,
10838 created_by: None,
10839 creation_date: None,
10840 info: irontide_core::InfoDict {
10841 name: "test".to_string(),
10842 piece_length,
10843 pieces: vec![0u8; num_pieces * 20],
10844 length: None,
10845 files: Some(file_entries),
10846 private: None,
10847 source: None,
10848 ssl_cert: None,
10849 similar: Vec::new(),
10850 collections: Vec::new(),
10851 },
10852 url_list: Vec::new(),
10853 httpseeds: Vec::new(),
10854 info_bytes: None,
10855 ssl_cert: None,
10856 }
10857 }
10858
10859 #[test]
10860 fn cached_files_populated_on_registration() {
10861 let meta = make_multi_file_meta(&[(100, "a.txt"), (200, "b.txt"), (50, "c.txt")], 100);
10867 let lengths = Lengths::new(350, 100, 16384);
10868 let cached = build_cached_file_info(&meta, &lengths);
10869
10870 assert_eq!(cached.entries.len(), 3);
10871
10872 assert_eq!(cached.entries[0].index, 0);
10873 assert_eq!(cached.entries[0].length, 100);
10874 assert_eq!(cached.entries[0].first_piece, 0);
10875 assert_eq!(cached.entries[0].last_piece, 0);
10876
10877 assert_eq!(cached.entries[1].index, 1);
10878 assert_eq!(cached.entries[1].length, 200);
10879 assert_eq!(cached.entries[1].first_piece, 1);
10880 assert_eq!(cached.entries[1].last_piece, 2);
10881
10882 assert_eq!(cached.entries[2].index, 2);
10883 assert_eq!(cached.entries[2].length, 50);
10884 assert_eq!(cached.entries[2].first_piece, 3);
10885 assert_eq!(cached.entries[2].last_piece, 3);
10886 }
10887
10888 #[test]
10889 fn cached_files_single_file_torrent() {
10890 let meta = TorrentMetaV1 {
10893 info_hash: Id20([0u8; 20]),
10894 announce: None,
10895 announce_list: None,
10896 comment: None,
10897 created_by: None,
10898 creation_date: None,
10899 info: irontide_core::InfoDict {
10900 name: "single.bin".to_string(),
10901 piece_length: 100,
10902 pieces: vec![0u8; 5 * 20],
10903 length: Some(500),
10904 files: None,
10905 private: None,
10906 source: None,
10907 ssl_cert: None,
10908 similar: Vec::new(),
10909 collections: Vec::new(),
10910 },
10911 url_list: Vec::new(),
10912 httpseeds: Vec::new(),
10913 info_bytes: None,
10914 ssl_cert: None,
10915 };
10916 let lengths = Lengths::new(500, 100, 16384);
10917 let cached = build_cached_file_info(&meta, &lengths);
10918
10919 assert_eq!(cached.entries.len(), 1);
10920 assert_eq!(cached.entries[0].index, 0);
10921 assert_eq!(cached.entries[0].length, 500);
10922 assert_eq!(cached.entries[0].first_piece, 0);
10923 assert_eq!(cached.entries[0].last_piece, 4);
10924 }
10925
10926 use crate::piece_reservation::{AtomicPieceStates, PieceState, StealCandidates};
10933 use irontide_storage::Bitfield;
10934
10935 fn steal_populate_scan(states: &AtomicPieceStates, sc: &StealCandidates) -> u32 {
10939 let mut pushed = 0u32;
10940 let num = states.len();
10941 for piece in 0..num {
10942 let state = states.get(piece);
10943 if state == PieceState::Reserved {
10944 sc.push(piece);
10945 pushed = pushed.saturating_add(1);
10946 }
10947 }
10948 pushed
10949 }
10950
10951 fn all_wanted(n: u32) -> Bitfield {
10952 let mut bf = Bitfield::new(n);
10953 for i in 0..n {
10954 bf.set(i);
10955 }
10956 bf
10957 }
10958
10959 #[test]
10960 fn steal_populate_pushes_reserved_pieces() {
10961 let n = 10;
10962 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
10963 let sc = StealCandidates::new();
10964
10965 assert!(states.try_reserve(2));
10967 assert!(states.try_reserve(5));
10968 assert!(states.try_reserve(7));
10969
10970 let pushed = steal_populate_scan(&states, &sc);
10971 assert_eq!(pushed, 3, "should push exactly the 3 reserved pieces");
10972
10973 let mut popped = Vec::new();
10975 while let Some(p) = sc.pop() {
10976 popped.push(p);
10977 }
10978 popped.sort_unstable();
10979 assert_eq!(popped, vec![2, 5, 7]);
10980 }
10981
10982 #[test]
10983 fn steal_populate_skips_non_reserved_states() {
10984 let n = 8;
10985 let mut have = Bitfield::new(n);
10986 have.set(0); let mut wanted = all_wanted(n);
10988 wanted.clear(1); let states = AtomicPieceStates::new(n, &have, &wanted);
10991 let sc = StealCandidates::new();
10992
10993 assert!(states.try_reserve(3));
10995
10996 let pushed = steal_populate_scan(&states, &sc);
10997 assert_eq!(pushed, 1, "only piece 3 (Reserved) should be pushed");
10998
10999 assert_eq!(sc.pop(), Some(3));
11000 assert_eq!(sc.pop(), None);
11001 }
11002
11003 #[test]
11004 fn steal_populate_deduplicates() {
11005 let n = 4;
11006 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11007 let sc = StealCandidates::new();
11008
11009 assert!(states.try_reserve(1));
11010 assert!(states.try_reserve(2));
11011
11012 let pushed1 = steal_populate_scan(&states, &sc);
11014 assert_eq!(pushed1, 2);
11015
11016 let pushed2 = steal_populate_scan(&states, &sc);
11019 assert_eq!(pushed2, 2, "scan still reports 2 reserved pieces");
11020
11021 let mut count = 0u32;
11022 while sc.pop().is_some() {
11023 count = count.saturating_add(1);
11024 }
11025 assert_eq!(count, 2, "dedup means only 2 entries despite 2 scans");
11026 }
11027
11028 #[test]
11029 fn steal_populate_skips_completed_pieces() {
11030 let n = 5;
11031 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11032 let sc = StealCandidates::new();
11033
11034 for i in 0..n {
11036 assert!(states.try_reserve(i));
11037 }
11038
11039 states.mark_complete(1);
11041 states.mark_complete(3);
11042
11043 let pushed = steal_populate_scan(&states, &sc);
11044 assert_eq!(pushed, 3, "3 pieces still Reserved (0, 2, 4)");
11045
11046 let mut popped = Vec::new();
11047 while let Some(p) = sc.pop() {
11048 popped.push(p);
11049 }
11050 popped.sort_unstable();
11051 assert_eq!(popped, vec![0, 2, 4]);
11052 }
11053
11054 #[test]
11055 fn steal_populate_empty_when_no_reserved() {
11056 let n = 6;
11057 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11058 let sc = StealCandidates::new();
11059
11060 let pushed = steal_populate_scan(&states, &sc);
11062 assert_eq!(pushed, 0);
11063 assert_eq!(sc.pop(), None);
11064 }
11065
11066 #[test]
11067 fn steal_populate_with_endgame_pieces() {
11068 let n = 4;
11070 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11071 let sc = StealCandidates::new();
11072
11073 assert!(states.try_reserve(0));
11074 assert!(states.try_reserve(1));
11075 states.transition_to_endgame(1);
11076
11077 let pushed = steal_populate_scan(&states, &sc);
11078 assert_eq!(
11079 pushed, 1,
11080 "only piece 0 (Reserved) should be pushed, not piece 1 (Endgame)"
11081 );
11082 assert_eq!(sc.pop(), Some(0));
11083 assert_eq!(sc.pop(), None);
11084 }
11085
11086 #[test]
11091 fn sync_piece_states_marks_unwanted_on_skip() {
11092 let n = 8;
11093 let mut wanted = all_wanted(n);
11094 wanted.clear(2);
11095 wanted.clear(3);
11096 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11097 assert_eq!(states.get(2), PieceState::Available);
11100 assert_eq!(states.get(3), PieceState::Available);
11101
11102 for piece in 0..n {
11104 let w = wanted.get(piece);
11105 let current = states.get(piece);
11106 if !w && current == PieceState::Available {
11107 states.mark_unwanted(piece);
11108 } else if w && current == PieceState::Unwanted {
11109 states.mark_available(piece);
11110 }
11111 }
11112
11113 assert_eq!(states.get(0), PieceState::Available);
11114 assert_eq!(states.get(2), PieceState::Unwanted);
11115 assert_eq!(states.get(3), PieceState::Unwanted);
11116 assert_eq!(states.get(4), PieceState::Available);
11117 }
11118
11119 #[test]
11120 fn sync_piece_states_restores_available_on_unskip() {
11121 let n = 6;
11122 let mut initial_wanted = all_wanted(n);
11123 initial_wanted.clear(1);
11124 initial_wanted.clear(4);
11125 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &initial_wanted);
11126 assert_eq!(states.get(1), PieceState::Unwanted);
11127 assert_eq!(states.get(4), PieceState::Unwanted);
11128
11129 let new_wanted = all_wanted(n);
11131 for piece in 0..n {
11132 let w = new_wanted.get(piece);
11133 let current = states.get(piece);
11134 if !w && current == PieceState::Available {
11135 states.mark_unwanted(piece);
11136 } else if w && current == PieceState::Unwanted {
11137 states.mark_available(piece);
11138 }
11139 }
11140
11141 assert_eq!(states.get(1), PieceState::Available);
11142 assert_eq!(states.get(4), PieceState::Available);
11143 }
11144
11145 #[test]
11146 fn sync_piece_states_shared_piece_stays_available() {
11147 let n = 4;
11151 let mut wanted = all_wanted(n);
11152 wanted.clear(0); let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11155
11156 for piece in 0..n {
11157 let w = wanted.get(piece);
11158 let current = states.get(piece);
11159 if !w && current == PieceState::Available {
11160 states.mark_unwanted(piece);
11161 } else if w && current == PieceState::Unwanted {
11162 states.mark_available(piece);
11163 }
11164 }
11165
11166 assert_eq!(states.get(0), PieceState::Unwanted);
11167 assert_eq!(
11168 states.get(1),
11169 PieceState::Available,
11170 "shared piece stays Available"
11171 );
11172 assert_eq!(states.get(2), PieceState::Available);
11173 assert_eq!(states.get(3), PieceState::Available);
11174 }
11175
11176 #[test]
11185 fn dht_requery_guard_scales_with_max_peers() {
11186 assert_eq!(128_usize.saturating_mul(4), 512);
11188
11189 assert_eq!(200_usize.saturating_mul(4), 800);
11191
11192 assert_eq!(50_usize.saturating_mul(4), 200);
11194
11195 assert_eq!(usize::MAX.saturating_mul(4), usize::MAX);
11197 }
11198
11199 fn make_test_info_bytes() -> (Vec<u8>, Id20) {
11203 use serde::Serialize;
11204
11205 #[derive(Serialize)]
11206 struct Info<'a> {
11207 length: u64,
11208 name: &'a str,
11209 #[serde(rename = "piece length")]
11210 piece_length: u64,
11211 #[serde(with = "serde_bytes")]
11212 pieces: &'a [u8],
11213 }
11214
11215 let data = vec![0xAB; 1024];
11216 let piece_hash = irontide_core::sha1(&data);
11217 let mut pieces = Vec::new();
11218 pieces.extend_from_slice(piece_hash.as_bytes());
11219
11220 let info = Info {
11221 length: 1024,
11222 name: "test",
11223 piece_length: 16384,
11224 pieces: &pieces,
11225 };
11226
11227 let info_bytes = irontide_bencode::to_bytes(&info).unwrap();
11228 let info_hash = irontide_core::sha1(&info_bytes);
11229 (info_bytes, info_hash)
11230 }
11231
11232 async fn create_magnet_handle(info_hash: Id20) -> TorrentHandle {
11234 let magnet = Magnet {
11235 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
11236 display_name: Some("test".into()),
11237 trackers: vec![],
11238 peers: vec![],
11239 selected_files: None,
11240 };
11241 let config = test_config();
11242 let (atx, amask) = test_alert_channel();
11243 let (dm, _dj) = test_disk_manager();
11244 TorrentHandle::from_magnet(
11245 magnet,
11246 dm,
11247 config,
11248 test_dht_rx(),
11249 test_dht_rx(),
11250 None,
11251 None,
11252 crate::slot_tuner::SlotTuner::disabled(4),
11253 atx,
11254 amask,
11255 None,
11256 None,
11257 test_ban_manager(),
11258 test_ip_filter(),
11259 Arc::new(Vec::new()),
11260 None,
11261 None,
11262 Arc::new(crate::transport::NetworkFactory::tokio()),
11263 None,
11264 Arc::new(crate::stats::SessionCounters::new()),
11265 )
11266 .await
11267 .unwrap()
11268 }
11269
11270 #[tokio::test]
11271 async fn pre_resolved_metadata_applies_when_fetching() {
11272 let (info_bytes, info_hash) = make_test_info_bytes();
11273 let handle = create_magnet_handle(info_hash).await;
11274
11275 let stats = handle.stats().await.unwrap();
11277 assert_eq!(stats.state, TorrentState::FetchingMetadata);
11278
11279 let peer_addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
11281 handle.send_pre_resolved_metadata(info_bytes, vec![peer_addr]);
11282
11283 tokio::time::sleep(Duration::from_millis(200)).await;
11285
11286 let stats = handle.stats().await.unwrap();
11288 assert_eq!(
11289 stats.state,
11290 TorrentState::Downloading,
11291 "should have transitioned to Downloading after pre-resolved metadata"
11292 );
11293 assert!(
11294 stats.pieces_total > 0,
11295 "should know piece count after metadata resolution"
11296 );
11297
11298 handle.shutdown().await.unwrap();
11299 }
11300
11301 #[tokio::test]
11302 async fn pre_resolved_metadata_ignored_after_resolution() {
11303 let data = vec![0xAB; 32768];
11305 let meta = make_test_torrent(&data, 16384);
11306 let info_hash = meta.info_hash;
11307 let storage = make_storage(&data, 16384);
11308 let config = test_config();
11309
11310 let (atx, amask) = test_alert_channel();
11311 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
11312 let handle = TorrentHandle::from_torrent(
11313 meta,
11314 irontide_core::TorrentVersion::V1Only,
11315 None,
11316 dh,
11317 dm,
11318 config,
11319 test_dht_rx(),
11320 test_dht_rx(),
11321 None,
11322 None,
11323 crate::slot_tuner::SlotTuner::disabled(4),
11324 atx,
11325 amask,
11326 None,
11327 None,
11328 test_ban_manager(),
11329 test_ip_filter(),
11330 Arc::new(Vec::new()),
11331 None,
11332 None,
11333 Arc::new(crate::transport::NetworkFactory::tokio()),
11334 None,
11335 Arc::new(crate::stats::SessionCounters::new()),
11336 )
11337 .await
11338 .unwrap();
11339
11340 let stats_before = handle.stats().await.unwrap();
11341 assert_eq!(stats_before.state, TorrentState::Downloading);
11342
11343 let (info_bytes, _) = make_test_info_bytes();
11346 handle.send_pre_resolved_metadata(info_bytes, vec![]);
11347
11348 tokio::time::sleep(Duration::from_millis(100)).await;
11350
11351 let stats_after = handle.stats().await.unwrap();
11353 assert_eq!(stats_after.state, TorrentState::Downloading);
11354 assert_eq!(stats_after.pieces_total, stats_before.pieces_total);
11355
11356 handle.shutdown().await.unwrap();
11357 }
11358
11359 #[tokio::test]
11360 async fn pre_resolved_metadata_with_invalid_hash_stays_fetching() {
11361 let (info_bytes, _correct_hash) = make_test_info_bytes();
11365
11366 let wrong_hash = Id20::from_hex("0000000000000000000000000000000000000001").unwrap();
11368 let handle = create_magnet_handle(wrong_hash).await;
11369
11370 let stats = handle.stats().await.unwrap();
11371 assert_eq!(stats.state, TorrentState::FetchingMetadata);
11372
11373 handle.send_pre_resolved_metadata(info_bytes, vec![]);
11375
11376 tokio::time::sleep(Duration::from_millis(200)).await;
11377
11378 let stats = handle.stats().await.unwrap();
11380 assert_eq!(
11381 stats.state,
11382 TorrentState::FetchingMetadata,
11383 "should stay in FetchingMetadata when info_hash doesn't match"
11384 );
11385
11386 handle.shutdown().await.unwrap();
11387 }
11388
11389 #[test]
11390 fn initial_queue_depth_is_128() {
11391 use crate::peer_shared::INITIAL_QUEUE_DEPTH;
11392 assert_eq!(INITIAL_QUEUE_DEPTH, 128);
11393 }
11394
11395 #[tokio::test]
11408 #[allow(
11409 clippy::large_stack_arrays,
11410 reason = "test data buffer passed directly to make_storage"
11411 )]
11412 async fn m159_seed_mode_suppresses_new_requests_on_wire() {
11413 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let info_hash = meta.info_hash;
11416 let storage = make_storage(&[0u8; 32768], 16384);
11418
11419 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
11420 let listen_addr = listener.local_addr().unwrap();
11421 let config = TorrentConfig {
11422 listen_port: listen_addr.port(),
11423 ..test_config()
11424 };
11425 drop(listener);
11426
11427 let (atx, amask) = test_alert_channel();
11428 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
11429 let handle = TorrentHandle::from_torrent(
11430 meta,
11431 irontide_core::TorrentVersion::V1Only,
11432 None,
11433 dh,
11434 dm,
11435 config,
11436 test_dht_rx(),
11437 test_dht_rx(),
11438 None,
11439 None,
11440 crate::slot_tuner::SlotTuner::disabled(4),
11441 atx,
11442 amask,
11443 None,
11444 None,
11445 test_ban_manager(),
11446 test_ip_filter(),
11447 Arc::new(Vec::new()),
11448 None,
11449 None,
11450 Arc::new(crate::transport::NetworkFactory::tokio()),
11451 None,
11452 Arc::new(crate::stats::SessionCounters::new()),
11453 )
11454 .await
11455 .unwrap();
11456
11457 tokio::time::sleep(Duration::from_millis(50)).await;
11458
11459 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
11461 let (reader, writer) = tokio::io::split(stream);
11462 let mut writer = writer;
11463 let mut reader = reader;
11464
11465 let hs = Handshake::new(
11466 info_hash,
11467 Id20::from_hex("dddddddddddddddddddddddddddddddddddddddd").unwrap(),
11468 );
11469 writer.write_all(&hs.to_bytes()).await.unwrap();
11470 writer.flush().await.unwrap();
11471 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
11472 reader.read_exact(&mut hs_buf).await.unwrap();
11473
11474 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
11475 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
11476
11477 let _actor_ext_hs = framed_read.next().await;
11479 let ext_hs = ExtHandshake::new();
11480 let ext_payload = ext_hs.to_bytes().unwrap();
11481 framed_write
11482 .send(Message::Extended {
11483 ext_id: 0,
11484 payload: ext_payload,
11485 })
11486 .await
11487 .unwrap();
11488
11489 let mut bf = Bitfield::new(2);
11491 bf.set(0);
11492 bf.set(1);
11493 framed_write
11494 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
11495 .await
11496 .unwrap();
11497 framed_write.send(Message::Unchoke).await.unwrap();
11498
11499 let mut initial_request_seen = false;
11503 let wait_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
11504 loop {
11505 let remaining = wait_deadline.saturating_duration_since(tokio::time::Instant::now());
11506 if remaining.is_zero() {
11507 break;
11508 }
11509 match tokio::time::timeout(remaining, framed_read.next()).await {
11510 Ok(Some(Ok(Message::Request { .. }))) => {
11511 initial_request_seen = true;
11512 break;
11513 }
11514 Ok(Some(Ok(_))) => {}
11515 _ => break,
11516 }
11517 }
11518 assert!(
11519 initial_request_seen,
11520 "actor should have sent a Request before seed mode toggle"
11521 );
11522
11523 handle.set_seed_mode(true).await.unwrap();
11526
11527 let grace_deadline = tokio::time::Instant::now() + Duration::from_millis(200);
11534 let mut cancel_seen = false;
11535 let mut grace_requests = 0u32;
11536 loop {
11537 let remaining = grace_deadline.saturating_duration_since(tokio::time::Instant::now());
11538 if remaining.is_zero() {
11539 break;
11540 }
11541 match tokio::time::timeout(remaining, framed_read.next()).await {
11542 Ok(Some(Ok(Message::Request { .. }))) => {
11543 grace_requests += 1;
11544 }
11545 Ok(Some(Ok(Message::Cancel { .. }))) => {
11546 cancel_seen = true;
11547 }
11548 Ok(Some(Ok(_))) => {}
11549 Ok(None | Some(Err(_))) | Err(_) => break,
11550 }
11551 }
11552 let _ = (cancel_seen, grace_requests);
11553
11554 let steady_deadline = tokio::time::Instant::now() + Duration::from_millis(500);
11557 let mut steady_requests = 0u32;
11558 loop {
11559 let remaining = steady_deadline.saturating_duration_since(tokio::time::Instant::now());
11560 if remaining.is_zero() {
11561 break;
11562 }
11563 match tokio::time::timeout(remaining, framed_read.next()).await {
11564 Ok(Some(Ok(Message::Request { .. }))) => {
11565 steady_requests += 1;
11566 }
11567 Ok(Some(Ok(_))) => {}
11568 Ok(None | Some(Err(_))) | Err(_) => break,
11569 }
11570 }
11571
11572 assert_eq!(
11573 steady_requests, 0,
11574 "after the Stop propagation grace window, no new Request messages \
11575 must appear during steady-state while user_seed_mode is active"
11576 );
11577
11578 let stats = handle.stats().await.unwrap();
11580 assert!(
11581 stats.user_seed_mode,
11582 "stats.user_seed_mode should be true after set_seed_mode(true)"
11583 );
11584
11585 handle.shutdown().await.unwrap();
11586 }
11587
11588 #[tokio::test]
11616 async fn m159_seed_mode_uploads_continue_on_wire() {
11617 const FILL_BYTE: u8 = 0x5A;
11618 const PIECE_LENGTH: u64 = 16384;
11619 const TOTAL_LEN: usize = 32768; let data = vec![FILL_BYTE; TOTAL_LEN];
11622 let meta = make_test_torrent(&data, PIECE_LENGTH);
11623 let info_hash = meta.info_hash;
11624 let storage = make_seeded_storage(&data, PIECE_LENGTH);
11626
11627 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
11628 let listen_addr = listener.local_addr().unwrap();
11629 let config = TorrentConfig {
11630 listen_port: listen_addr.port(),
11631 ..test_config()
11632 };
11633 drop(listener);
11634
11635 let (atx, amask) = test_alert_channel();
11636 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
11637 let handle = TorrentHandle::from_torrent(
11638 meta,
11639 irontide_core::TorrentVersion::V1Only,
11640 None,
11641 dh,
11642 dm,
11643 config,
11644 test_dht_rx(),
11645 test_dht_rx(),
11646 None,
11647 None,
11648 crate::slot_tuner::SlotTuner::disabled(4),
11649 atx,
11650 amask,
11651 None,
11652 None,
11653 test_ban_manager(),
11654 test_ip_filter(),
11655 Arc::new(Vec::new()),
11656 None,
11657 None,
11658 Arc::new(crate::transport::NetworkFactory::tokio()),
11659 None,
11660 Arc::new(crate::stats::SessionCounters::new()),
11661 )
11662 .await
11663 .unwrap();
11664
11665 let seeding_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
11668 loop {
11669 tokio::time::sleep(Duration::from_millis(50)).await;
11670 let stats = handle.stats().await.unwrap();
11671 if stats.state == TorrentState::Seeding && stats.pieces_have == 2 {
11672 break;
11673 }
11674 if tokio::time::Instant::now() > seeding_deadline {
11675 let stats = handle.stats().await.unwrap();
11676 panic!(
11677 "actor did not reach Seeding state within 3s: state={:?}, have={}/{}",
11678 stats.state, stats.pieces_have, stats.pieces_total
11679 );
11680 }
11681 }
11682
11683 handle.set_seed_mode(true).await.unwrap();
11686 let stats = handle.stats().await.unwrap();
11687 assert!(
11688 stats.user_seed_mode,
11689 "stats.user_seed_mode should be true after set_seed_mode(true)"
11690 );
11691
11692 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
11694 let (reader, writer) = tokio::io::split(stream);
11695 let mut writer = writer;
11696 let mut reader = reader;
11697
11698 let hs = Handshake::new(
11699 info_hash,
11700 Id20::from_hex("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(),
11701 );
11702 writer.write_all(&hs.to_bytes()).await.unwrap();
11703 writer.flush().await.unwrap();
11704 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
11705 reader.read_exact(&mut hs_buf).await.unwrap();
11706
11707 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
11708 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
11709
11710 let _actor_ext_hs = framed_read.next().await;
11712 let ext_hs = ExtHandshake::new();
11713 let ext_payload = ext_hs.to_bytes().unwrap();
11714 framed_write
11715 .send(Message::Extended {
11716 ext_id: 0,
11717 payload: ext_payload,
11718 })
11719 .await
11720 .unwrap();
11721
11722 let bf = Bitfield::new(2);
11724 framed_write
11725 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
11726 .await
11727 .unwrap();
11728 framed_write.send(Message::Interested).await.unwrap();
11729
11730 let unchoke_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
11734 let mut saw_unchoke = false;
11735 loop {
11736 let remaining = unchoke_deadline.saturating_duration_since(tokio::time::Instant::now());
11737 if remaining.is_zero() {
11738 break;
11739 }
11740 match tokio::time::timeout(remaining, framed_read.next()).await {
11741 Ok(Some(Ok(Message::Unchoke))) => {
11742 saw_unchoke = true;
11743 break;
11744 }
11745 Ok(Some(Ok(_))) => {}
11746 Ok(None | Some(Err(_))) => break,
11747 Err(_elapsed) => break,
11748 }
11749 }
11750 assert!(
11751 saw_unchoke,
11752 "actor should have unchoked the leecher while user_seed_mode is active"
11753 );
11754
11755 framed_write
11758 .send(Message::Request {
11759 index: 0,
11760 begin: 0,
11761 length: PIECE_LENGTH as u32,
11762 })
11763 .await
11764 .unwrap();
11765
11766 let piece_deadline = tokio::time::Instant::now() + Duration::from_secs(2);
11771 let mut got_piece = false;
11772 loop {
11773 let remaining = piece_deadline.saturating_duration_since(tokio::time::Instant::now());
11774 if remaining.is_zero() {
11775 break;
11776 }
11777 match tokio::time::timeout(remaining, framed_read.next()).await {
11778 Ok(Some(Ok(Message::Piece {
11779 index,
11780 begin,
11781 data_0,
11782 data_1,
11783 }))) => {
11784 assert_eq!(index, 0, "Piece index should match request");
11785 assert_eq!(begin, 0, "Piece begin should match request");
11786 let mut payload: Vec<u8> =
11787 Vec::with_capacity(data_0.len().saturating_add(data_1.len()));
11788 payload.extend_from_slice(&data_0);
11789 payload.extend_from_slice(&data_1);
11790 assert_eq!(
11791 payload.len(),
11792 PIECE_LENGTH as usize,
11793 "Piece payload length should match requested length"
11794 );
11795 assert!(
11796 payload.iter().all(|&b| b == FILL_BYTE),
11797 "Piece payload should contain the pre-seeded fill byte"
11798 );
11799 got_piece = true;
11800 break;
11801 }
11802 Ok(Some(Ok(_))) => {}
11803 Ok(None | Some(Err(_))) => break,
11804 Err(_elapsed) => break,
11805 }
11806 }
11807 assert!(
11808 got_piece,
11809 "actor should have served a Piece in response to Request while user_seed_mode is active"
11810 );
11811
11812 let stats = handle.stats().await.unwrap();
11815 assert!(
11816 stats.user_seed_mode,
11817 "stats.user_seed_mode should remain true after serving an upload"
11818 );
11819 assert!(
11820 stats.uploaded >= u64::from(PIECE_LENGTH as u32),
11821 "stats.uploaded should reflect the served block, got {}",
11822 stats.uploaded
11823 );
11824
11825 handle.shutdown().await.unwrap();
11826 }
11827
11828 #[tokio::test]
11831 async fn info_field_populated_for_torrent() {
11832 let data = vec![0xAB; 32768];
11833 let meta = make_test_torrent(&data, 16384);
11834 let storage = make_storage(&data, 16384);
11835 let config = test_config();
11836
11837 let (atx, amask) = test_alert_channel();
11838 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
11839 let handle = TorrentHandle::from_torrent(
11840 meta,
11841 irontide_core::TorrentVersion::V1Only,
11842 None,
11843 dh,
11844 dm,
11845 config,
11846 test_dht_rx(),
11847 test_dht_rx(),
11848 None,
11849 None,
11850 crate::slot_tuner::SlotTuner::disabled(4),
11851 atx,
11852 amask,
11853 None,
11854 None,
11855 test_ban_manager(),
11856 test_ip_filter(),
11857 Arc::new(Vec::new()),
11858 None,
11859 None,
11860 Arc::new(crate::transport::NetworkFactory::tokio()),
11861 None,
11862 Arc::new(crate::stats::SessionCounters::new()),
11863 )
11864 .await
11865 .unwrap();
11866
11867 tokio::time::sleep(Duration::from_millis(50)).await;
11868
11869 let rd = handle.save_resume_data().await.unwrap();
11870
11871 assert!(rd.info.is_some(), "rd.info should be Some for .torrent");
11873
11874 let info_bytes = rd.info.as_ref().unwrap();
11876 let info: irontide_core::InfoDict =
11877 irontide_bencode::from_bytes(info_bytes).expect("info bytes should deserialize");
11878 assert_eq!(info.name, "test");
11879 assert_eq!(info.piece_length, 16384);
11880
11881 handle.shutdown().await.unwrap();
11882 }
11883
11884 #[tokio::test]
11885 async fn info_hash2_none_for_v1_only() {
11886 let data = vec![0xCD; 16384];
11887 let meta = make_test_torrent(&data, 16384);
11888 let storage = make_storage(&data, 16384);
11889 let config = test_config();
11890
11891 let (atx, amask) = test_alert_channel();
11892 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
11893 let handle = TorrentHandle::from_torrent(
11894 meta,
11895 irontide_core::TorrentVersion::V1Only,
11896 None,
11897 dh,
11898 dm,
11899 config,
11900 test_dht_rx(),
11901 test_dht_rx(),
11902 None,
11903 None,
11904 crate::slot_tuner::SlotTuner::disabled(4),
11905 atx,
11906 amask,
11907 None,
11908 None,
11909 test_ban_manager(),
11910 test_ip_filter(),
11911 Arc::new(Vec::new()),
11912 None,
11913 None,
11914 Arc::new(crate::transport::NetworkFactory::tokio()),
11915 None,
11916 Arc::new(crate::stats::SessionCounters::new()),
11917 )
11918 .await
11919 .unwrap();
11920
11921 tokio::time::sleep(Duration::from_millis(50)).await;
11922
11923 let rd = handle.save_resume_data().await.unwrap();
11924
11925 assert!(
11927 rd.info_hash2.is_none(),
11928 "v1-only torrent should have info_hash2 = None"
11929 );
11930
11931 assert!(
11933 rd.added_time > 0,
11934 "added_time should be a positive POSIX timestamp"
11935 );
11936
11937 handle.shutdown().await.unwrap();
11938 }
11939
11940 #[tokio::test]
11941 async fn info_none_for_unresolved_magnet() {
11942 let magnet = Magnet {
11943 info_hashes: irontide_core::InfoHashes::v1_only(
11944 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
11945 ),
11946 display_name: Some("magnet-test".into()),
11947 trackers: vec![],
11948 peers: vec![],
11949 selected_files: None,
11950 };
11951 let config = test_config();
11952
11953 let (atx, amask) = test_alert_channel();
11954 let (dm, _dj) = test_disk_manager();
11955 let handle = TorrentHandle::from_magnet(
11956 magnet,
11957 dm,
11958 config,
11959 test_dht_rx(),
11960 test_dht_rx(),
11961 None,
11962 None,
11963 crate::slot_tuner::SlotTuner::disabled(4),
11964 atx,
11965 amask,
11966 None,
11967 None,
11968 test_ban_manager(),
11969 test_ip_filter(),
11970 Arc::new(Vec::new()),
11971 None,
11972 None,
11973 Arc::new(crate::transport::NetworkFactory::tokio()),
11974 None,
11975 Arc::new(crate::stats::SessionCounters::new()),
11976 )
11977 .await
11978 .unwrap();
11979
11980 tokio::time::sleep(Duration::from_millis(50)).await;
11981
11982 let rd = handle.save_resume_data().await.unwrap();
11983
11984 assert!(
11986 rd.info.is_none(),
11987 "unresolved magnet should have info = None"
11988 );
11989
11990 assert!(
11992 rd.added_time > 0,
11993 "added_time should be set for magnet links"
11994 );
11995
11996 handle.shutdown().await.unwrap();
11997 }
11998
11999 #[tokio::test]
12002 async fn torrent_command_get_meta_returns_none_before_metadata() {
12003 let info_hash =
12005 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").expect("valid hex");
12006 let handle = create_magnet_handle(info_hash).await;
12007
12008 let (tx, rx) = oneshot::channel();
12009 handle
12010 .cmd_tx
12011 .send(TorrentCommand::GetMeta { reply: tx })
12012 .await
12013 .expect("cmd_tx send");
12014 let result = rx.await.expect("GetMeta reply");
12015 assert!(
12016 result.is_none(),
12017 "pre-metadata magnet must return None from GetMeta"
12018 );
12019
12020 handle.shutdown().await.unwrap();
12021 }
12022
12023 #[tokio::test]
12024 async fn torrent_command_get_meta_returns_some_after_metadata() {
12025 let (info_bytes, info_hash) = make_test_info_bytes();
12028 let handle = create_magnet_handle(info_hash).await;
12029
12030 handle.send_pre_resolved_metadata(info_bytes, vec![]);
12031
12032 let mut result = None;
12036 for _ in 0..100 {
12037 tokio::time::sleep(Duration::from_millis(20)).await;
12038 let (tx, rx) = oneshot::channel();
12039 handle
12040 .cmd_tx
12041 .send(TorrentCommand::GetMeta { reply: tx })
12042 .await
12043 .expect("cmd_tx send");
12044 let r = rx.await.expect("GetMeta reply");
12045 if r.is_some() {
12046 result = r;
12047 break;
12048 }
12049 }
12050 let meta = result.expect("GetMeta must return Some after PreResolvedMetadata");
12051 assert_eq!(meta.info_hash, info_hash);
12052
12053 handle.shutdown().await.unwrap();
12054 }
12055
12056 #[tokio::test]
12059 async fn web_seed_progress_idle_to_active_on_first_success() {
12060 let mut actor = TorrentActor::for_throttle_test(8, 0);
12061 actor.handle_web_seed_progress("http://seed.example/file", 1024, 1_000_000, None);
12062 let stats = actor
12063 .web_seed_stats
12064 .get("http://seed.example/file")
12065 .expect("stats inserted");
12066 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12067 assert_eq!(stats.downloaded_bytes, 1024);
12068 assert_eq!(stats.last_rate_bps, 1_000_000);
12069 assert_eq!(stats.consecutive_failures, 0);
12070 assert!(stats.last_attempt_unix_secs > 0);
12071 assert!(actor.need_save_resume);
12072 }
12073
12074 #[tokio::test]
12075 async fn web_seed_progress_active_to_errored_then_recovery_persists_last_error() {
12076 let mut actor = TorrentActor::for_throttle_test(8, 0);
12077 let url = "http://seed.example/file".to_string();
12078
12079 actor.handle_web_seed_progress(&url, 1024, 100, None);
12081 assert_eq!(
12082 actor.web_seed_stats[&url].state,
12083 irontide_core::WebSeedState::Active
12084 );
12085
12086 actor.handle_web_seed_progress(&url, 1024, 0, Some("503".into()));
12088 let stats = &actor.web_seed_stats[&url];
12089 assert_eq!(stats.state, irontide_core::WebSeedState::Errored);
12090 assert_eq!(stats.last_error.as_deref(), Some("503"));
12091 assert_eq!(stats.consecutive_failures, 1);
12092
12093 actor.handle_web_seed_progress(&url, 2048, 200, None);
12095 let stats = &actor.web_seed_stats[&url];
12096 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12097 assert_eq!(
12098 stats.last_error.as_deref(),
12099 Some("503"),
12100 "last_error must persist through recovery (D-eng-8)"
12101 );
12102 assert_eq!(
12103 stats.consecutive_failures, 0,
12104 "consecutive_failures resets on success"
12105 );
12106 }
12107
12108 #[tokio::test]
12109 async fn web_seed_progress_consecutive_failures_monotonic_within_run() {
12110 let mut actor = TorrentActor::for_throttle_test(8, 0);
12111 let url = "http://seed.example/file".to_string();
12112
12113 actor.handle_web_seed_progress(&url, 0, 0, Some("e1".into()));
12114 actor.handle_web_seed_progress(&url, 0, 0, Some("e2".into()));
12115 actor.handle_web_seed_progress(&url, 0, 0, Some("e3".into()));
12116 let stats = &actor.web_seed_stats[&url];
12117 assert_eq!(stats.consecutive_failures, 3);
12118 assert_eq!(
12119 stats.last_error.as_deref(),
12120 Some("e3"),
12121 "last_error reflects most recent message"
12122 );
12123
12124 actor.handle_web_seed_progress(&url, 1024, 100, None);
12125 assert_eq!(
12126 actor.web_seed_stats[&url].consecutive_failures, 0,
12127 "success resets consecutive_failures"
12128 );
12129 }
12130
12131 fn install_peer_states(actor: &mut TorrentActor) {
12136 let (queue_tx, _queue_rx) = mpsc::unbounded_channel();
12137 actor.peer_states = Some(std::sync::Arc::new(crate::peer_states::PeerStates::new(
12138 queue_tx,
12139 )));
12140 }
12141
12142 fn addr(octet: u8, port: u16) -> std::net::SocketAddr {
12143 std::net::SocketAddr::new(
12144 std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 0, 2, octet)),
12145 port,
12146 )
12147 }
12148
12149 #[tokio::test]
12150 async fn pex_count_dedups_same_peer_in_two_messages() {
12151 let mut actor = TorrentActor::for_throttle_test(8, 0);
12152 install_peer_states(&mut actor);
12153
12154 actor.handle_add_peers(
12156 vec![addr(1, 6881), addr(2, 6881)],
12157 crate::peer_state::PeerSource::Pex,
12158 );
12159 actor.handle_add_peers(
12161 vec![addr(1, 6881), addr(3, 6881)],
12162 crate::peer_state::PeerSource::Pex,
12163 );
12164 assert_eq!(
12165 actor.pex_peer_count, 3,
12166 "3 unique peers across 2 PEX messages, A counted once"
12167 );
12168 assert_eq!(actor.lsd_peer_count, 0, "LSD untouched");
12169 }
12170
12171 #[tokio::test]
12172 async fn lsd_count_aggregates_across_multicasts() {
12173 let mut actor = TorrentActor::for_throttle_test(8, 0);
12174 install_peer_states(&mut actor);
12175
12176 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd);
12177 actor.handle_add_peers(
12178 vec![addr(2, 6881), addr(3, 6881)],
12179 crate::peer_state::PeerSource::Lsd,
12180 );
12181 actor.handle_add_peers(
12182 vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd,
12184 );
12185 assert_eq!(actor.lsd_peer_count, 3);
12186 }
12187
12188 #[tokio::test]
12189 async fn other_sources_do_not_bump_pex_or_lsd() {
12190 let mut actor = TorrentActor::for_throttle_test(8, 0);
12191 install_peer_states(&mut actor);
12192
12193 actor.handle_add_peers(
12194 vec![addr(1, 6881), addr(2, 6881)],
12195 crate::peer_state::PeerSource::Tracker,
12196 );
12197 actor.handle_add_peers(vec![addr(3, 6881)], crate::peer_state::PeerSource::Dht);
12198 actor.handle_add_peers(vec![addr(4, 6881)], crate::peer_state::PeerSource::Incoming);
12199 assert_eq!(actor.pex_peer_count, 0);
12200 assert_eq!(actor.lsd_peer_count, 0);
12201 }
12202
12203 #[tokio::test]
12204 async fn dedup_runs_against_global_seen_set() {
12205 let mut actor = TorrentActor::for_throttle_test(8, 0);
12211 install_peer_states(&mut actor);
12212
12213 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Tracker);
12214 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Pex);
12215 assert_eq!(
12216 actor.pex_peer_count, 0,
12217 "peer already seen via tracker — PEX shouldn't re-count"
12218 );
12219 }
12220
12221 #[tokio::test]
12222 async fn web_seed_progress_dirties_resume_flag() {
12223 let mut actor = TorrentActor::for_throttle_test(8, 0);
12224 actor.need_save_resume = false;
12225 actor.handle_web_seed_progress("http://x/file", 100, 50, None);
12226 assert!(
12227 actor.need_save_resume,
12228 "every progress event should mark fast-resume dirty"
12229 );
12230 }
12231
12232 #[tokio::test]
12233 async fn paused_torrent_rejects_outbound_peer_connect() {
12234 let mut actor = TorrentActor::for_throttle_test(8, 0);
12235 install_peer_states(&mut actor);
12236 actor.state = TorrentState::Paused;
12237
12238 let sem = Arc::new(tokio::sync::Semaphore::new(1));
12239 let permit = sem.clone().acquire_owned().await.unwrap();
12240 let connect = crate::peer_adder::ConnectPeer {
12241 addr: addr(1, 6881),
12242 source: crate::peer_state::PeerSource::Dht,
12243 permit,
12244 };
12245 actor.handle_adder_connect(connect);
12246 assert!(
12247 actor.peers.is_empty(),
12248 "paused torrent must not accept outbound peer connections"
12249 );
12250 assert_eq!(
12251 sem.available_permits(),
12252 1,
12253 "semaphore permit must be released on rejection"
12254 );
12255 }
12256
12257 #[tokio::test]
12258 async fn resume_from_queued_restores_fetching_metadata_for_magnets() {
12259 let mut actor = TorrentActor::for_throttle_test(0, 0);
12260 actor.state = TorrentState::Queued;
12261 assert!(
12262 actor.chunk_tracker.is_none(),
12263 "magnet torrent has no chunk tracker before metadata"
12264 );
12265 assert_eq!(actor.num_pieces, 0);
12266
12267 actor.handle_resume().await;
12268 assert_eq!(
12269 actor.state,
12270 TorrentState::FetchingMetadata,
12271 "magnet torrent must resume to FetchingMetadata, not Downloading"
12272 );
12273 }
12274
12275 #[tokio::test]
12276 async fn resume_from_queued_restores_downloading_when_metadata_known() {
12277 let mut actor = TorrentActor::for_throttle_test(8, 0);
12278 actor.state = TorrentState::Queued;
12279
12280 actor.handle_resume().await;
12281 assert_eq!(
12282 actor.state,
12283 TorrentState::Downloading,
12284 "torrent with known pieces must resume to Downloading"
12285 );
12286 }
12287
12288 #[tokio::test]
12289 async fn queued_torrent_rejects_outbound_peer_connect() {
12290 let mut actor = TorrentActor::for_throttle_test(8, 0);
12291 install_peer_states(&mut actor);
12292 actor.state = TorrentState::Queued;
12293
12294 let sem = Arc::new(tokio::sync::Semaphore::new(1));
12295 let permit = sem.clone().acquire_owned().await.unwrap();
12296 let connect = crate::peer_adder::ConnectPeer {
12297 addr: addr(1, 6881),
12298 source: crate::peer_state::PeerSource::Dht,
12299 permit,
12300 };
12301 actor.handle_adder_connect(connect);
12302 assert!(
12303 actor.peers.is_empty(),
12304 "queued torrent must not accept outbound peer connections"
12305 );
12306 assert_eq!(
12307 sem.available_permits(),
12308 1,
12309 "semaphore permit must be released on rejection"
12310 );
12311 }
12312
12313 fn inject_peer_for_flush(
12317 actor: &mut TorrentActor,
12318 peer_addr: std::net::SocketAddr,
12319 unchoke_started: Option<std::time::Instant>,
12320 prior_total: std::time::Duration,
12321 ) {
12322 let (cmd_tx, _cmd_rx) = mpsc::channel(8);
12323 let mut peer = crate::peer_state::PeerState::new(
12324 peer_addr,
12325 actor.num_pieces,
12326 cmd_tx,
12327 crate::peer_state::PeerSource::Tracker,
12328 Arc::new(AtomicU32::new(0)),
12329 Arc::new(AtomicU32::new(128)),
12330 Arc::new(tokio::sync::Notify::new()),
12331 );
12332 peer.am_unchoke_started_at = unchoke_started;
12333 peer.unchoke_duration_total = prior_total;
12334 actor.peers.insert(peer_addr, peer);
12335 }
12336
12337 #[tokio::test]
12338 async fn disconnect_while_unchoked_flushes_delta_into_torrent_map() {
12339 let mut actor = TorrentActor::for_throttle_test(8, 0);
12340 let p = addr(1, 6881);
12341
12342 inject_peer_for_flush(
12345 &mut actor,
12346 p,
12347 Some(std::time::Instant::now() - std::time::Duration::from_millis(50)),
12348 std::time::Duration::from_millis(100),
12349 );
12350
12351 actor.disconnect_peer(p, "test");
12352
12353 let total = actor
12354 .unchoke_durations
12355 .get(&p)
12356 .copied()
12357 .expect("disconnect must flush a non-zero delta into the torrent map");
12358 assert!(
12359 total >= std::time::Duration::from_millis(140),
12360 "expected ≥140 ms (100 prior + ~50 in-flight), got {total:?}"
12361 );
12362 }
12363
12364 #[tokio::test]
12365 async fn disconnect_then_reconnect_preserves_history() {
12366 let mut actor = TorrentActor::for_throttle_test(8, 0);
12367 let p = addr(2, 6881);
12368
12369 inject_peer_for_flush(&mut actor, p, None, std::time::Duration::from_millis(80));
12371 actor.disconnect_peer(p, "test");
12372 let after_first = *actor
12373 .unchoke_durations
12374 .get(&p)
12375 .expect("first flush must populate the entry");
12376 assert_eq!(after_first, std::time::Duration::from_millis(80));
12377
12378 inject_peer_for_flush(
12380 &mut actor,
12381 p,
12382 Some(std::time::Instant::now() - std::time::Duration::from_millis(40)),
12383 std::time::Duration::ZERO,
12384 );
12385 actor.disconnect_peer(p, "test");
12386 let after_second = *actor.unchoke_durations.get(&p).unwrap();
12387 assert!(
12388 after_second >= std::time::Duration::from_millis(120),
12389 "second flush must add to the existing entry, got {after_second:?}"
12390 );
12391 }
12392
12393 #[tokio::test]
12396 async fn piece_verified_wakes_reservation_notify() {
12397 let mut actor = TorrentActor::for_throttle_test(8, 0);
12398 let notify = Arc::new(tokio::sync::Notify::new());
12399 actor.reservation_notify = Some(Arc::clone(¬ify));
12400
12401 let notified = notify.notified();
12402 tokio::pin!(notified);
12403 assert!(
12404 futures::poll!(&mut notified).is_pending(),
12405 "notify should not have fired yet"
12406 );
12407
12408 actor.on_piece_verified(0).await;
12409
12410 tokio::time::timeout(Duration::from_secs(1), notified)
12411 .await
12412 .expect("reservation_notify must be woken by on_piece_verified");
12413 }
12414
12415 fn actor_with_tracker_state(queue: u32, inflight: u32) -> TorrentActor {
12421 use crate::piece_reservation::PieceTracker;
12422 use irontide_storage::Bitfield;
12423 let mut actor = TorrentActor::for_throttle_test(8, 0);
12424 let num_pieces = queue + inflight + 1;
12425 let we_have = Bitfield::new(num_pieces);
12426 let mut wanted = Bitfield::new(num_pieces);
12427 for i in 0..num_pieces {
12428 wanted.set(i);
12429 }
12430 let mut pt = PieceTracker::new(num_pieces, &we_have, &wanted);
12431 for i in queue..num_pieces {
12434 pt.mark_unwanted(i);
12435 }
12436 for i in 0..inflight {
12438 pt.record_reservation(i, "10.0.0.1:6881".parse().unwrap());
12439 }
12440 actor.piece_tracker = Some(pt);
12445 actor
12446 }
12447
12448 #[tokio::test]
12449 async fn pipeline_tick_skips_wake_when_dispatch_state_unchanged() {
12450 let mut actor = actor_with_tracker_state(10, 3);
12451 let notify = Arc::new(tokio::sync::Notify::new());
12452 actor.reservation_notify = Some(Arc::clone(¬ify));
12453
12454 actor.tick_dispatch_safety_wake();
12458 let _drain = notify.notified();
12459
12460 let notified = notify.notified();
12462 tokio::pin!(notified);
12463 actor.tick_dispatch_safety_wake();
12464
12465 tokio::task::yield_now().await;
12467 assert!(
12468 futures::poll!(&mut notified).is_pending(),
12469 "tick must not wake when (queue_count, inflight_count) is unchanged"
12470 );
12471 let skipped = actor.counters.get(crate::stats::DISPATCH_TICK_WAKE_SKIPPED);
12473 assert!(
12474 skipped >= 1,
12475 "expected DISPATCH_TICK_WAKE_SKIPPED >= 1, got {skipped}"
12476 );
12477 }
12478
12479 #[tokio::test]
12480 async fn pipeline_tick_wakes_when_inflight_changes() {
12481 let mut actor = actor_with_tracker_state(10, 3);
12482 let notify = Arc::new(tokio::sync::Notify::new());
12483 actor.reservation_notify = Some(Arc::clone(¬ify));
12484
12485 actor.tick_dispatch_safety_wake();
12487
12488 if let Some(ref mut pt) = actor.piece_tracker {
12491 pt.record_reservation(5, "10.0.0.2:6881".parse().unwrap());
12492 }
12493
12494 let notified = notify.notified();
12495 tokio::pin!(notified);
12496 actor.tick_dispatch_safety_wake();
12497
12498 tokio::time::timeout(Duration::from_secs(1), notified)
12499 .await
12500 .expect("tick must wake when dispatch state changed");
12501 }
12502}