1#![allow(
8 clippy::cast_possible_truncation,
9 clippy::cast_precision_loss,
10 clippy::cast_possible_wrap,
11 clippy::cast_sign_loss,
12 clippy::unchecked_time_subtraction,
13 reason = "M175: piece/peer arithmetic bounded by num_pieces/max_peers (u32); time deltas use post-init Instants; qBt DTOs follow wire-format integer widths"
14)]
15
16use std::collections::{BTreeSet, HashMap, HashSet};
23use std::net::SocketAddr;
24
25use rustc_hash::FxHashMap;
26use std::sync::Arc;
27use std::sync::atomic::AtomicU32;
28use std::time::{Duration, Instant};
29
30use bytes::Bytes;
31use tokio::sync::{broadcast, mpsc, oneshot};
32use tracing::{Instrument, debug, info, trace, warn};
33
34use crate::alert::{Alert, AlertKind, post_alert};
35use crate::disk::{DiskHandle, DiskJobFlags, DiskManagerHandle};
36use crate::piece_reservation::{
37 AtomicPieceStates, BlockMaps, PieceOrderMap, PieceTracker, StealCandidates,
38};
39
40use irontide_core::{
41 DEFAULT_CHUNK_SIZE, FilePriority, Id20, Lengths, Magnet, PeerId, TorrentMetaV1,
42 torrent_from_bytes,
43};
44use irontide_storage::{Bitfield, ChunkTracker, MemoryStorage, TorrentStorage};
49
50use crate::choker::{Choker, PeerInfo as ChokerPeerInfo};
51use crate::end_game::EndGame;
52use crate::metadata::MetadataDownloader;
53use crate::peer_adder::{self, ConnectPeer};
54use crate::peer_state::{PeerSource, PeerState};
55use crate::tracker_manager::TrackerManager;
56use crate::types::{
57 PartialPieceInfo, PeerCommand, PeerEvent, PeerInfo, TorrentCommand, TorrentConfig,
58 TorrentState, TorrentStats,
59};
60
61pub(crate) type SharedBucket = Arc<parking_lot::Mutex<crate::rate_limiter::TokenBucket>>;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub(crate) enum HashResult {
71 Passed,
73 Failed,
75 NotApplicable,
77}
78
79pub(crate) fn relocate_files(
85 src_base: &std::path::Path,
86 dst_base: &std::path::Path,
87 file_paths: &[std::path::PathBuf],
88) -> std::io::Result<()> {
89 for rel_path in file_paths {
90 let src = src_base.join(rel_path);
91 let dst = dst_base.join(rel_path);
92
93 if !src.exists() {
94 continue;
96 }
97
98 if let Some(parent) = dst.parent() {
99 std::fs::create_dir_all(parent)?;
100 }
101
102 if std::fs::rename(&src, &dst).is_err() {
104 std::fs::copy(&src, &dst)?;
106 std::fs::remove_file(&src)?;
107 }
108 }
109
110 for rel_path in file_paths {
113 let mut dir = src_base.join(rel_path);
114 dir.pop(); while dir != *src_base {
116 if std::fs::remove_dir(&dir).is_err() {
117 break; }
119 dir.pop();
120 }
121 }
122
123 Ok(())
124}
125
126pub(crate) fn initial_unchoke_slots(max_uploads_per_torrent: i32) -> usize {
130 if max_uploads_per_torrent >= 1 {
131 max_uploads_per_torrent as usize
132 } else {
133 4
134 }
135}
136
137pub(crate) fn now_unix() -> i64 {
139 std::time::SystemTime::now()
140 .duration_since(std::time::UNIX_EPOCH)
141 .map_or(0, |d| d.as_secs() as i64)
142}
143
144pub(crate) const HOLEPUNCH_COOLDOWN: Duration = Duration::from_mins(2);
146
147pub(crate) const HOLEPUNCH_MAX_TRACKED: usize = 256;
149
150pub(crate) const HOLEPUNCH_RELAY_MAX_PER_WINDOW: u32 = 5;
152
153pub(crate) const HOLEPUNCH_RELAY_WINDOW: Duration = Duration::from_secs(30);
155
156pub(crate) fn should_attempt_holepunch(reason: &str) -> bool {
159 if reason.contains("holepunch") {
161 return false;
162 }
163 reason.contains("refused")
164 || reason.contains("timed out")
165 || reason.contains("Connection reset")
166 || reason.contains("connection reset")
167}
168
169#[derive(Clone)]
171pub struct TorrentHandle {
172 pub cmd_tx: mpsc::Sender<TorrentCommand>,
173}
174
175impl TorrentHandle {
176 #[allow(clippy::too_many_arguments)]
188 pub async fn from_torrent(
189 meta: TorrentMetaV1,
190 version: irontide_core::TorrentVersion,
191 meta_v2: Option<irontide_core::TorrentMetaV2>,
192 disk: DiskHandle,
193 disk_manager: DiskManagerHandle,
194 config: TorrentConfig,
195 dht_rx: irontide_dht::DhtReceiver,
196 dht_v6_rx: irontide_dht::DhtReceiver,
197 global_upload_bucket: Option<SharedBucket>,
198 global_download_bucket: Option<SharedBucket>,
199 slot_tuner: crate::slot_tuner::SlotTuner,
200 alert_tx: broadcast::Sender<Alert>,
201 alert_mask: Arc<AtomicU32>,
202 utp_socket: Option<irontide_utp::UtpSocket>,
203 utp_socket_v6: Option<irontide_utp::UtpSocket>,
204 ban_manager: irontide_session_types::SharedBanManager,
205 ip_filter: irontide_session_types::SharedIpFilter,
206 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
207 sam_session: Option<Arc<crate::i2p::SamSession>>,
208 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
209 factory: Arc<crate::transport::NetworkFactory>,
210 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
211 counters: Arc<crate::stats::SessionCounters>,
212 ) -> crate::Result<Self> {
213 let mut config = config;
214 if meta.info.private == Some(1) {
216 config.enable_dht = false;
217 config.enable_pex = false;
218 config.enable_lsd = false;
219 }
220
221 let info_hashes = match (&version, &meta_v2) {
222 (irontide_core::TorrentVersion::Hybrid, Some(v2_meta)) => {
223 if let Some(v2_hash) = v2_meta.info_hashes.v2 {
224 irontide_core::InfoHashes::hybrid(meta.info_hash, v2_hash)
225 } else {
226 irontide_core::InfoHashes::v1_only(meta.info_hash)
227 }
228 }
229 (irontide_core::TorrentVersion::V2Only, Some(v2_meta)) => v2_meta.info_hashes.clone(),
230 _ => irontide_core::InfoHashes::v1_only(meta.info_hash),
231 };
232
233 if meta.info.piece_length > config.max_piece_length {
234 return Err(crate::Error::InvalidSettings(format!(
235 "piece_length {} exceeds max_piece_length {}",
236 meta.info.piece_length, config.max_piece_length
237 )));
238 }
239
240 let num_pieces = meta.info.num_pieces() as u32;
241 let lengths = Lengths::new(
242 meta.info.total_length(),
243 meta.info.piece_length,
244 DEFAULT_CHUNK_SIZE,
245 );
246 let mut chunk_tracker = ChunkTracker::new(lengths.clone());
247
248 let hash_picker = if version.has_v2() {
250 if let Some(ref v2_meta) = meta_v2 {
251 chunk_tracker.enable_v2_tracking();
252
253 let block_size = 16384u64;
254 let blocks_per_piece = (meta.info.piece_length / block_size) as u32;
255
256 let v2_files = v2_meta.info.files();
258 let file_infos: Vec<irontide_core::FileHashInfo> = v2_files
259 .iter()
260 .filter_map(|f| {
261 let root = f.attr.pieces_root?;
262 let num_blocks = f.attr.length.div_ceil(block_size) as u32;
263 let num_pieces = f.attr.length.div_ceil(meta.info.piece_length) as u32;
264 Some(irontide_core::FileHashInfo {
265 root,
266 num_blocks,
267 num_pieces,
268 })
269 })
270 .collect();
271
272 if file_infos.is_empty() {
273 None
274 } else {
275 let mut picker = irontide_core::HashPicker::new(&file_infos, blocks_per_piece);
276
277 let _verified = picker.load_piece_layers(&v2_meta.piece_layers);
279
280 Some(picker)
281 }
282 } else {
283 None
284 }
285 } else {
286 None
287 };
288
289 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
290 let mut file_priorities = config.file_priorities.clone();
292 file_priorities.resize(file_lengths.len(), FilePriority::Normal);
293 let wanted_pieces =
294 crate::piece_selector::build_wanted_pieces(&file_priorities, &file_lengths, &lengths);
295
296 let (cmd_tx, cmd_rx) = mpsc::channel(256);
297 let (event_tx, event_rx) = mpsc::channel(2048);
298 let (write_error_tx, write_error_rx) = mpsc::channel(64);
299 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
300 let (hash_result_tx, hash_result_rx) = mpsc::channel(64); let our_peer_id = if config.anonymous_mode {
302 PeerId::generate_anonymous().0
303 } else {
304 PeerId::generate().0
305 };
306
307 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
310 .bind_tcp(SocketAddr::from((
311 std::net::Ipv6Addr::UNSPECIFIED,
312 config.listen_port,
313 )))
314 .await
315 {
316 Ok(l) => Some(l),
317 Err(_) => factory
318 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
319 .await
320 .ok(),
321 };
322 let mut tracker_manager = TrackerManager::from_torrent_filtered(
325 &meta,
326 our_peer_id,
327 config.listen_port,
328 config.url_security,
329 config.peer_dscp,
330 config.anonymous_mode,
331 );
332 tracker_manager.set_info_hashes(info_hashes.clone());
333
334 if let Some(ref sam) = sam_session {
336 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
337 }
338
339 let enable_dht = config.enable_dht;
340
341 let dht_initial = dht_rx.current();
346 let dht_v6_initial = dht_v6_rx.current();
347
348 let dht_peers_rx = if enable_dht {
350 if let Some(ref dht) = dht_initial {
351 match dht.get_peers(meta.info_hash).await {
352 Ok(rx) => Some(rx),
353 Err(e) => {
354 warn!("failed to start DHT v4 get_peers: {e}");
355 None
356 }
357 }
358 } else {
359 None
360 }
361 } else {
362 None
363 };
364
365 let dht_v6_peers_rx = if enable_dht {
366 if let Some(ref dht6) = dht_v6_initial {
367 match dht6.get_peers(meta.info_hash).await {
368 Ok(rx) => Some(rx),
369 Err(e) => {
370 debug!("failed to start DHT v6 get_peers: {e}");
371 None
372 }
373 }
374 } else {
375 None
376 }
377 } else {
378 None
379 };
380
381 let v2_as_v1 = if info_hashes.is_hybrid() {
383 info_hashes
384 .v2
385 .map(|v2| Id20(v2.0[..20].try_into().unwrap()))
386 } else {
387 None
388 };
389 let (dht_v2_peers_rx, dht_v6_v2_peers_rx) =
390 if let (true, Some(v2_id)) = (enable_dht, v2_as_v1) {
391 let rx4 = if let Some(ref dht) = dht_initial {
392 dht.get_peers(v2_id).await.ok()
393 } else {
394 None
395 };
396 let rx6 = if let Some(ref dht6) = dht_v6_initial {
397 dht6.get_peers(v2_id).await.ok()
398 } else {
399 None
400 };
401 (rx4, rx6)
402 } else {
403 (None, None)
404 };
405
406 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
407 let download_bucket = Arc::new(parking_lot::Mutex::new(
408 crate::rate_limiter::TokenBucket::new(config.download_rate_limit),
409 ));
410 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
411 0,
412 0,
413 0,
414 0,
415 config.upload_rate_limit,
416 config.download_rate_limit,
417 );
418
419 let super_seed = if config.super_seeding {
420 Some(crate::super_seed::SuperSeedState::new())
421 } else {
422 None
423 };
424 let (have_broadcast_tx, _) =
426 tokio::sync::broadcast::channel(std::cmp::max(128, num_pieces as usize / 4));
427 let is_share_mode = config.share_mode;
428
429 let (piece_ready_tx, _) = broadcast::channel(64);
430 let initial_have = chunk_tracker.bitfield().clone();
431 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(initial_have);
432 let stream_read_semaphore =
433 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
434
435 let choker = Choker::with_algorithms(
436 initial_unchoke_slots(config.max_uploads_per_torrent),
437 config.seed_choking_algorithm,
438 config.choking_algorithm,
439 config.upload_rate_limit,
440 2,
441 20,
442 );
443
444 let mut disk = disk;
446 if matches!(version, irontide_core::TorrentVersion::V1Only)
447 && let Some(pool) = &hash_pool
448 {
449 disk.set_hash_pool(pool.clone());
450 disk.set_hash_result_tx(hash_result_tx.clone());
451 }
452
453 let cached_files = Some(build_cached_file_info(&meta, &lengths));
455
456 let (order_map_tx, _order_map_rx_seed) =
458 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
459
460 let actor = TorrentActor {
461 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
462 config.lock_warn_threshold_ms,
463 ),
464 config,
465 info_hash: meta.info_hash,
466 our_peer_id,
467 state: TorrentState::Downloading,
468 disk: Some(disk),
469 disk_manager,
470 chunk_tracker: Some(chunk_tracker),
471 lengths: Some(lengths),
472 num_pieces,
473 streaming_pieces: BTreeSet::new(),
474 time_critical_pieces: BTreeSet::new(),
475 streaming_cursors: Vec::new(),
476 piece_ready_tx,
477 have_watch_tx,
478 have_watch_rx,
479 stream_read_semaphore,
480 file_priorities,
481 wanted_pieces,
482 end_game: EndGame::new(),
483 peers: HashMap::new(),
484 unchoke_durations: HashMap::new(),
485 cached_peer_rates: FxHashMap::default(),
486 refill_notify: Arc::new(tokio::sync::Notify::new()),
487 atomic_states: None,
488 block_maps: None,
489 steal_candidates: None,
490 last_steal_populate: Instant::now(),
491 piece_write_guards: None,
492 soft_reap_buf: Vec::new(),
493 eviction_history: std::collections::VecDeque::new(),
494 force_immediate_choker_tick: false,
495 piece_tracker: None,
496 order_map_dirty: false,
497 next_order_map_gen: 0,
498 order_map_tx,
499 piece_owner: Vec::new(),
500 peer_slab: crate::piece_reservation::PeerSlab::new(),
501 priority_pieces: BTreeSet::new(),
502 max_in_flight: 512,
503 reservation_notify: None,
504 last_tick_dispatch_state: None,
505 choker,
506 user_seed_mode: false,
507 user_forced: false,
508 max_connections: 0,
509 peer_states: None,
510 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
511 connect_permits: HashMap::new(),
512 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
513 std::collections::HashMap::new(),
514 )),
515 connect_rx: None,
516 metadata_downloader: None,
517 downloaded: 0,
518 uploaded: 0,
519 checking_progress: 0.0,
520 total_download: 0,
521 total_upload: 0,
522 total_failed_bytes: 0,
523 total_redundant_bytes: 0,
524 added_time: std::time::SystemTime::now()
525 .duration_since(std::time::UNIX_EPOCH)
526 .map_or(0, |d| d.as_secs() as i64),
527 completed_time: 0,
528 last_download: 0,
529 last_upload: 0,
530 last_seen_complete: 0,
531 active_duration: 0,
532 finished_duration: 0,
533 seeding_duration: 0,
534 active_since: Some(std::time::Instant::now()),
535 state_duration_since: None,
536 started_at: std::time::Instant::now(),
537 moving_storage: false,
538 has_incoming: false,
539 need_save_resume: false,
540 error: String::new(),
541 error_file: -1,
542 cmd_rx,
543 event_tx,
544 event_rx,
545 write_error_rx,
546 write_error_tx,
547 verify_result_rx,
548 verify_result_tx,
549 pending_verify: HashSet::new(),
550 piece_generations: vec![0u64; num_pieces as usize],
551 hash_result_rx,
552 hash_result_tx,
553 meta: Some(meta),
554 cached_files,
555 listener,
556 utp_socket,
557 utp_socket_v6,
558 tracker_manager,
559 tracker_result_rx: None,
560 dht_rx,
561 dht_v6_rx,
562 dht_enabled: enable_dht,
563 dht_peers_rx,
564 dht_v6_peers_rx,
565 dht_v6_empty_count: 0,
566 dht_v6_last_retry: None,
567 alert_tx,
568 alert_mask,
569 upload_bucket,
570 download_bucket,
571 global_upload_bucket,
572 global_download_bucket,
573 slot_tuner,
574 upload_bytes_interval: 0,
575 peak_download_rate: 0,
576 rechoke_per_min_est: 0.0,
577 web_seeds: HashMap::new(),
578 banned_web_seeds: HashSet::new(),
579 web_seed_in_flight: HashMap::new(),
580 web_seed_stats: HashMap::new(),
581 pex_peer_count: 0,
582 lsd_peer_count: 0,
583 super_seed,
584 have_broadcast_tx,
585 suggested_to_peers: HashMap::new(),
586 predictive_have_sent: HashSet::new(),
587
588 ban_manager,
589 ip_filter,
590 piece_contributors: HashMap::new(),
591 parole_pieces: HashMap::new(),
592 external_ip: None,
593 share_lru: std::collections::VecDeque::new(),
594 share_max_pieces: if is_share_mode { 64 } else { 0 },
595 plugins,
596 hash_picker,
597 version,
598 meta_v2,
599 info_hashes,
600 dht_v2_peers_rx,
601 dht_v6_v2_peers_rx,
602 magnet_selected_files: None,
603 sam_session,
604 i2p_accept_rx: None,
605 i2p_peer_counter: 0,
606 i2p_destinations: HashMap::new(),
607 ssl_manager,
608 rate_limiter_set,
609 auto_sequential_active: false,
610 factory,
611 hash_pool_ref: hash_pool,
612 connect_attempts: 0,
613 connect_failures: 0,
614 choke_rotations: 0,
615 inflight_started: Vec::new(),
616 completed_piece_times: std::collections::VecDeque::new(),
617 piece_steals: 0,
618 holepunch_relayed: 0,
619 holepunch_relay_rate: HashMap::new(),
620 holepunch_cooldowns: HashMap::new(),
621 holepunch_pending: Vec::new(),
622 counters,
623 };
624
625 let spawn_info_hash = actor.info_hash;
626 let join_handle = tokio::spawn(actor.run());
627 tokio::spawn(async move {
629 match join_handle.await {
630 Ok(()) => {
631 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
632 }
633 Err(e) if e.is_panic() => {
634 let panic_payload = e.into_panic();
635 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
636 (*s).to_string()
637 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
638 s.clone()
639 } else {
640 "unknown panic payload".to_string()
641 };
642 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
643 }
644 Err(e) => {
645 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
646 }
647 }
648 });
649 Ok(Self { cmd_tx })
650 }
651
652 #[allow(clippy::too_many_arguments)]
661 pub async fn from_magnet(
662 magnet: Magnet,
663 disk_manager: DiskManagerHandle,
664 config: TorrentConfig,
665 dht_rx: irontide_dht::DhtReceiver,
666 dht_v6_rx: irontide_dht::DhtReceiver,
667 global_upload_bucket: Option<SharedBucket>,
668 global_download_bucket: Option<SharedBucket>,
669 slot_tuner: crate::slot_tuner::SlotTuner,
670 alert_tx: broadcast::Sender<Alert>,
671 alert_mask: Arc<AtomicU32>,
672 utp_socket: Option<irontide_utp::UtpSocket>,
673 utp_socket_v6: Option<irontide_utp::UtpSocket>,
674 ban_manager: irontide_session_types::SharedBanManager,
675 ip_filter: irontide_session_types::SharedIpFilter,
676 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
677 sam_session: Option<Arc<crate::i2p::SamSession>>,
678 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
679 factory: Arc<crate::transport::NetworkFactory>,
680 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
681 counters: Arc<crate::stats::SessionCounters>,
682 ) -> crate::Result<Self> {
683 let (cmd_tx, cmd_rx) = mpsc::channel(256);
684 let (event_tx, event_rx) = mpsc::channel(2048);
685 let (write_error_tx, write_error_rx) = mpsc::channel(64);
686 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
687 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
689 let our_peer_id = if config.anonymous_mode {
690 PeerId::generate_anonymous().0
691 } else {
692 PeerId::generate().0
693 };
694
695 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
697 .bind_tcp(SocketAddr::from((
698 std::net::Ipv6Addr::UNSPECIFIED,
699 config.listen_port,
700 )))
701 .await
702 {
703 Ok(l) => Some(l),
704 Err(_) => factory
705 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
706 .await
707 .ok(),
708 };
709 let mut tracker_manager = TrackerManager::empty(
712 magnet.info_hash(),
713 our_peer_id,
714 config.listen_port,
715 config.peer_dscp,
716 config.anonymous_mode,
717 );
718 for url in &magnet.trackers {
720 tracker_manager.add_tracker_url(url);
721 }
722
723 if let Some(ref sam) = sam_session {
725 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
726 }
727
728 let enable_dht = config.enable_dht;
729
730 let dht_initial = dht_rx.current();
734 let dht_v6_initial = dht_v6_rx.current();
735
736 let dht_peers_rx = if enable_dht {
738 if let Some(ref dht) = dht_initial {
739 match dht.get_peers(magnet.info_hash()).await {
740 Ok(rx) => Some(rx),
741 Err(e) => {
742 warn!("failed to start DHT v4 get_peers: {e}");
743 None
744 }
745 }
746 } else {
747 None
748 }
749 } else {
750 None
751 };
752
753 let dht_v6_peers_rx = if enable_dht {
754 if let Some(ref dht6) = dht_v6_initial {
755 match dht6.get_peers(magnet.info_hash()).await {
756 Ok(rx) => Some(rx),
757 Err(e) => {
758 debug!("failed to start DHT v6 get_peers: {e}");
759 None
760 }
761 }
762 } else {
763 None
764 }
765 } else {
766 None
767 };
768
769 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
770 let download_bucket = Arc::new(parking_lot::Mutex::new(
771 crate::rate_limiter::TokenBucket::new(config.download_rate_limit),
772 ));
773 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
774 0,
775 0,
776 0,
777 0,
778 config.upload_rate_limit,
779 config.download_rate_limit,
780 );
781
782 let super_seed = if config.super_seeding {
783 Some(crate::super_seed::SuperSeedState::new())
784 } else {
785 None
786 };
787 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
789 let is_share_mode = config.share_mode;
790 let magnet_selected_files = magnet.selected_files.clone();
791 let info_hashes = magnet.info_hashes.clone();
792
793 let (piece_ready_tx, _) = broadcast::channel(64);
794 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(0));
795 let stream_read_semaphore =
796 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
797
798 let choker = Choker::with_algorithms(
799 initial_unchoke_slots(config.max_uploads_per_torrent),
800 config.seed_choking_algorithm,
801 config.choking_algorithm,
802 config.upload_rate_limit,
803 2,
804 20,
805 );
806
807 let (order_map_tx, _order_map_rx_seed) =
808 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
809
810 let actor = TorrentActor {
811 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
812 config.lock_warn_threshold_ms,
813 ),
814 config,
815 info_hash: magnet.info_hash(),
816 our_peer_id,
817 state: TorrentState::FetchingMetadata,
818 disk: None,
819 disk_manager,
820 chunk_tracker: None,
821 lengths: None,
822 num_pieces: 0,
823 streaming_pieces: BTreeSet::new(),
824 time_critical_pieces: BTreeSet::new(),
825 streaming_cursors: Vec::new(),
826 piece_ready_tx,
827 have_watch_tx,
828 have_watch_rx,
829 stream_read_semaphore,
830 file_priorities: Vec::new(),
831 wanted_pieces: Bitfield::new(0),
832 end_game: EndGame::new(),
833 peers: HashMap::new(),
834 unchoke_durations: HashMap::new(),
835 cached_peer_rates: FxHashMap::default(),
836 refill_notify: Arc::new(tokio::sync::Notify::new()),
837 atomic_states: None,
838 block_maps: None,
839 steal_candidates: None,
840 last_steal_populate: Instant::now(),
841 piece_write_guards: None,
842 soft_reap_buf: Vec::new(),
843 eviction_history: std::collections::VecDeque::new(),
844 force_immediate_choker_tick: false,
845 piece_tracker: None,
846 order_map_dirty: false,
847 next_order_map_gen: 0,
848 order_map_tx,
849 piece_owner: Vec::new(),
850 peer_slab: crate::piece_reservation::PeerSlab::new(),
851 priority_pieces: BTreeSet::new(),
852 max_in_flight: 512,
853 reservation_notify: None,
854 last_tick_dispatch_state: None,
855 choker,
856 user_seed_mode: false,
857 user_forced: false,
858 max_connections: 0,
859 peer_states: None,
860 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
861 connect_permits: HashMap::new(),
862 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
863 std::collections::HashMap::new(),
864 )),
865 connect_rx: None,
866 metadata_downloader: Some(MetadataDownloader::new(magnet.info_hash())),
867 downloaded: 0,
868 uploaded: 0,
869 checking_progress: 0.0,
870 total_download: 0,
871 total_upload: 0,
872 total_failed_bytes: 0,
873 total_redundant_bytes: 0,
874 added_time: std::time::SystemTime::now()
875 .duration_since(std::time::UNIX_EPOCH)
876 .map_or(0, |d| d.as_secs() as i64),
877 completed_time: 0,
878 last_download: 0,
879 last_upload: 0,
880 last_seen_complete: 0,
881 active_duration: 0,
882 finished_duration: 0,
883 seeding_duration: 0,
884 active_since: Some(std::time::Instant::now()),
885 state_duration_since: None,
886 started_at: std::time::Instant::now(),
887 moving_storage: false,
888 has_incoming: false,
889 need_save_resume: false,
890 error: String::new(),
891 error_file: -1,
892 cmd_rx,
893 event_tx,
894 event_rx,
895 write_error_rx,
896 write_error_tx,
897 verify_result_rx,
898 verify_result_tx,
899 pending_verify: HashSet::new(),
900 piece_generations: Vec::new(),
901 hash_result_rx,
902 hash_result_tx,
903 meta: None,
904 cached_files: None,
905 listener,
906 utp_socket,
907 utp_socket_v6,
908 tracker_manager,
909 tracker_result_rx: None,
910 dht_rx,
911 dht_v6_rx,
912 dht_enabled: enable_dht,
913 dht_peers_rx,
914 dht_v6_peers_rx,
915 dht_v6_empty_count: 0,
916 dht_v6_last_retry: None,
917 alert_tx,
918 alert_mask,
919 upload_bucket,
920 download_bucket,
921 global_upload_bucket,
922 global_download_bucket,
923 slot_tuner,
924 upload_bytes_interval: 0,
925 peak_download_rate: 0,
926 rechoke_per_min_est: 0.0,
927 web_seeds: HashMap::new(),
928 banned_web_seeds: HashSet::new(),
929 web_seed_in_flight: HashMap::new(),
930 web_seed_stats: HashMap::new(),
931 pex_peer_count: 0,
932 lsd_peer_count: 0,
933 super_seed,
934 have_broadcast_tx,
935 suggested_to_peers: HashMap::new(),
936 predictive_have_sent: HashSet::new(),
937
938 ban_manager,
939 ip_filter,
940 piece_contributors: HashMap::new(),
941 parole_pieces: HashMap::new(),
942 external_ip: None,
943 share_lru: std::collections::VecDeque::new(),
944 share_max_pieces: if is_share_mode { 64 } else { 0 },
945 plugins,
946 hash_picker: None,
947 version: irontide_core::TorrentVersion::V1Only,
948 meta_v2: None,
949 info_hashes,
950 dht_v2_peers_rx: None,
951 dht_v6_v2_peers_rx: None,
952 magnet_selected_files,
953 sam_session,
954 i2p_accept_rx: None,
955 i2p_peer_counter: 0,
956 i2p_destinations: HashMap::new(),
957 ssl_manager,
958 rate_limiter_set,
959 auto_sequential_active: false,
960 factory,
961 hash_pool_ref: hash_pool,
962 connect_attempts: 0,
963 connect_failures: 0,
964 choke_rotations: 0,
965 inflight_started: Vec::new(),
966 completed_piece_times: std::collections::VecDeque::new(),
967 piece_steals: 0,
968 holepunch_relayed: 0,
969 holepunch_relay_rate: HashMap::new(),
970 holepunch_cooldowns: HashMap::new(),
971 holepunch_pending: Vec::new(),
972 counters,
973 };
974
975 let spawn_info_hash = actor.info_hash;
976 let join_handle = tokio::spawn(actor.run());
977 tokio::spawn(async move {
978 match join_handle.await {
979 Ok(()) => {
980 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
981 }
982 Err(e) if e.is_panic() => {
983 let panic_payload = e.into_panic();
984 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
985 (*s).to_string()
986 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
987 s.clone()
988 } else {
989 "unknown panic payload".to_string()
990 };
991 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
992 }
993 Err(e) => {
994 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
995 }
996 }
997 });
998 Ok(Self { cmd_tx })
999 }
1000
1001 pub async fn send_incoming_peer(
1006 &self,
1007 stream: crate::transport::BoxedStream,
1008 addr: SocketAddr,
1009 ) -> crate::Result<()> {
1010 self.cmd_tx
1011 .send(TorrentCommand::IncomingPeer { stream, addr })
1012 .await
1013 .map_err(|_| crate::Error::Shutdown)
1014 }
1015
1016 pub async fn stats(&self) -> crate::Result<TorrentStats> {
1022 let (tx, rx) = oneshot::channel();
1023 self.cmd_tx
1024 .send(TorrentCommand::Stats { reply: tx })
1025 .await
1026 .map_err(|_| crate::Error::Shutdown)?;
1027 rx.await.map_err(|_| crate::Error::Shutdown)
1028 }
1029
1030 pub async fn get_meta(&self) -> crate::Result<Option<TorrentMetaV1>> {
1046 let (tx, rx) = oneshot::channel();
1047 self.cmd_tx
1048 .send(TorrentCommand::GetMeta { reply: tx })
1049 .await
1050 .map_err(|_| crate::Error::Shutdown)?;
1051 rx.await.map_err(|_| crate::Error::Shutdown)
1052 }
1053
1054 pub async fn add_peers(&self, peers: Vec<SocketAddr>, source: PeerSource) -> crate::Result<()> {
1060 self.cmd_tx
1061 .send(TorrentCommand::AddPeers { peers, source })
1062 .await
1063 .map_err(|_| crate::Error::Shutdown)
1064 }
1065
1066 pub async fn pause(&self) -> crate::Result<()> {
1072 self.cmd_tx
1073 .send(TorrentCommand::Pause)
1074 .await
1075 .map_err(|_| crate::Error::Shutdown)
1076 }
1077
1078 pub async fn queue(&self) -> crate::Result<()> {
1084 self.cmd_tx
1085 .send(TorrentCommand::Queue)
1086 .await
1087 .map_err(|_| crate::Error::Shutdown)
1088 }
1089
1090 pub async fn set_category(&self, category: Option<String>) -> crate::Result<()> {
1099 let (tx, rx) = oneshot::channel();
1100 self.cmd_tx
1101 .send(TorrentCommand::SetCategory {
1102 category,
1103 reply: tx,
1104 })
1105 .await
1106 .map_err(|_| crate::Error::Shutdown)?;
1107 rx.await.map_err(|_| crate::Error::Shutdown)
1108 }
1109
1110 pub async fn set_tags(&self, tags: Vec<String>) -> crate::Result<()> {
1121 let (tx, rx) = oneshot::channel();
1122 self.cmd_tx
1123 .send(TorrentCommand::SetTags { tags, reply: tx })
1124 .await
1125 .map_err(|_| crate::Error::Shutdown)?;
1126 rx.await.map_err(|_| crate::Error::Shutdown)
1127 }
1128
1129 pub async fn resume(&self) -> crate::Result<()> {
1135 self.cmd_tx
1136 .send(TorrentCommand::Resume)
1137 .await
1138 .map_err(|_| crate::Error::Shutdown)
1139 }
1140
1141 pub async fn shutdown(&self) -> crate::Result<()> {
1147 let _ = tokio::time::timeout(
1150 std::time::Duration::from_secs(5),
1151 self.cmd_tx.send(TorrentCommand::Shutdown),
1152 )
1153 .await;
1154 Ok(())
1155 }
1156
1157 pub async fn save_resume_data(&self) -> crate::Result<irontide_core::FastResumeData> {
1163 let (tx, rx) = oneshot::channel();
1164 self.cmd_tx
1165 .send(TorrentCommand::SaveResumeData { reply: tx })
1166 .await
1167 .map_err(|_| crate::Error::Shutdown)?;
1168 rx.await.map_err(|_| crate::Error::Shutdown)?
1169 }
1170
1171 pub async fn clear_save_resume_flag(&self) -> crate::Result<()> {
1176 self.cmd_tx
1177 .send(TorrentCommand::ClearSaveResumeFlag)
1178 .await
1179 .map_err(|_| crate::Error::Shutdown)
1180 }
1181
1182 pub async fn take_resume_if_dirty(
1199 &self,
1200 ) -> crate::Result<Option<irontide_core::FastResumeData>> {
1201 let (tx, rx) = oneshot::channel();
1202 self.cmd_tx
1203 .send(TorrentCommand::TakeResumeIfDirty { reply: tx })
1204 .await
1205 .map_err(|_| crate::Error::Shutdown)?;
1206 rx.await.map_err(|_| crate::Error::Shutdown)?
1207 }
1208
1209 pub async fn mark_resume_dirty(&self) -> crate::Result<()> {
1219 self.cmd_tx
1220 .send(TorrentCommand::MarkResumeDirty)
1221 .await
1222 .map_err(|_| crate::Error::Shutdown)
1223 }
1224
1225 pub async fn restore_resume_bitmap(&self, pieces: Vec<u8>) -> crate::Result<()> {
1236 let (tx, rx) = oneshot::channel();
1237 self.cmd_tx
1238 .send(TorrentCommand::RestoreResumeBitmap { pieces, reply: tx })
1239 .await
1240 .map_err(|_| crate::Error::Shutdown)?;
1241 rx.await.map_err(|_| crate::Error::Shutdown)?
1242 }
1243
1244 pub async fn restore_web_seed_stats(
1250 &self,
1251 stats: HashMap<String, irontide_core::WebSeedStats>,
1252 ) -> crate::Result<()> {
1253 let (tx, rx) = oneshot::channel();
1254 self.cmd_tx
1255 .send(TorrentCommand::RestoreWebSeedStats { stats, reply: tx })
1256 .await
1257 .map_err(|_| crate::Error::Shutdown)?;
1258 rx.await.map_err(|_| crate::Error::Shutdown)?
1259 }
1260
1261 pub async fn peer_source_counts(&self) -> crate::Result<(usize, usize)> {
1269 let (tx, rx) = oneshot::channel();
1270 self.cmd_tx
1271 .send(TorrentCommand::GetPeerSourceCounts { reply: tx })
1272 .await
1273 .map_err(|_| crate::Error::Shutdown)?;
1274 rx.await.map_err(|_| crate::Error::Shutdown)
1275 }
1276
1277 pub async fn query_unchoke_durations(
1283 &self,
1284 ) -> crate::Result<HashMap<SocketAddr, std::time::Duration>> {
1285 let (tx, rx) = oneshot::channel();
1286 self.cmd_tx
1287 .send(TorrentCommand::QueryUnchokeDurations { reply: tx })
1288 .await
1289 .map_err(|_| crate::Error::Shutdown)?;
1290 rx.await.map_err(|_| crate::Error::Shutdown)
1291 }
1292
1293 pub async fn get_web_seed_stats(&self) -> crate::Result<Vec<irontide_core::WebSeedStats>> {
1299 let (tx, rx) = oneshot::channel();
1300 self.cmd_tx
1301 .send(TorrentCommand::GetWebSeedStats { reply: tx })
1302 .await
1303 .map_err(|_| crate::Error::Shutdown)?;
1304 rx.await.map_err(|_| crate::Error::Shutdown)
1305 }
1306
1307 pub async fn set_file_priority(
1313 &self,
1314 index: usize,
1315 priority: irontide_core::FilePriority,
1316 ) -> crate::Result<()> {
1317 let (tx, rx) = oneshot::channel();
1318 self.cmd_tx
1319 .send(TorrentCommand::SetFilePriority {
1320 index,
1321 priority,
1322 reply: tx,
1323 })
1324 .await
1325 .map_err(|_| crate::Error::Shutdown)?;
1326 rx.await.map_err(|_| crate::Error::Shutdown)?
1327 }
1328
1329 pub async fn file_priorities(&self) -> crate::Result<Vec<irontide_core::FilePriority>> {
1335 let (tx, rx) = oneshot::channel();
1336 self.cmd_tx
1337 .send(TorrentCommand::FilePriorities { reply: tx })
1338 .await
1339 .map_err(|_| crate::Error::Shutdown)?;
1340 rx.await.map_err(|_| crate::Error::Shutdown)
1341 }
1342
1343 pub async fn tracker_list(&self) -> crate::Result<Vec<crate::tracker_manager::TrackerInfo>> {
1349 let (tx, rx) = oneshot::channel();
1350 self.cmd_tx
1351 .send(TorrentCommand::TrackerList { reply: tx })
1352 .await
1353 .map_err(|_| crate::Error::Shutdown)?;
1354 rx.await.map_err(|_| crate::Error::Shutdown)
1355 }
1356
1357 pub async fn get_web_seeds(&self) -> crate::Result<Vec<String>> {
1363 let (tx, rx) = oneshot::channel();
1364 self.cmd_tx
1365 .send(TorrentCommand::GetWebSeeds { reply: tx })
1366 .await
1367 .map_err(|_| crate::Error::Shutdown)?;
1368 rx.await.map_err(|_| crate::Error::Shutdown)
1369 }
1370
1371 pub async fn get_piece_states(&self) -> crate::Result<Vec<u8>> {
1377 let (tx, rx) = oneshot::channel();
1378 self.cmd_tx
1379 .send(TorrentCommand::GetPieceStates { reply: tx })
1380 .await
1381 .map_err(|_| crate::Error::Shutdown)?;
1382 rx.await.map_err(|_| crate::Error::Shutdown)
1383 }
1384
1385 pub async fn get_piece_hashes(&self, offset: u32, limit: u32) -> crate::Result<Vec<String>> {
1395 let (tx, rx) = oneshot::channel();
1396 self.cmd_tx
1397 .send(TorrentCommand::GetPieceHashes {
1398 offset,
1399 limit,
1400 reply: tx,
1401 })
1402 .await
1403 .map_err(|_| crate::Error::Shutdown)?;
1404 let raw = rx.await.map_err(|_| crate::Error::Shutdown)?;
1408 Ok(raw.iter().map(hex::encode).collect())
1409 }
1410
1411 pub async fn force_reannounce(&self) -> crate::Result<()> {
1417 self.cmd_tx
1418 .send(TorrentCommand::ForceReannounce)
1419 .await
1420 .map_err(|_| crate::Error::Shutdown)
1421 }
1422
1423 pub async fn scrape(&self) -> crate::Result<Option<(String, irontide_tracker::ScrapeInfo)>> {
1429 let (tx, rx) = oneshot::channel();
1430 self.cmd_tx
1431 .send(TorrentCommand::Scrape { reply: tx })
1432 .await
1433 .map_err(|_| crate::Error::Shutdown)?;
1434 rx.await.map_err(|_| crate::Error::Shutdown)
1435 }
1436
1437 pub async fn open_file(
1443 &self,
1444 file_index: usize,
1445 ) -> crate::Result<crate::streaming::FileStream> {
1446 let (tx, rx) = oneshot::channel();
1447 self.cmd_tx
1448 .send(TorrentCommand::OpenFile {
1449 file_index,
1450 reply: tx,
1451 })
1452 .await
1453 .map_err(|_| crate::Error::Shutdown)?;
1454 let handle = rx.await.map_err(|_| crate::Error::Shutdown)??;
1455 Ok(crate::streaming::FileStream::from_handle(handle))
1456 }
1457
1458 pub async fn update_external_ip(&self, ip: std::net::IpAddr) -> crate::Result<()> {
1463 self.cmd_tx
1464 .send(TorrentCommand::UpdateExternalIp { ip })
1465 .await
1466 .map_err(|_| crate::Error::Shutdown)
1467 }
1468
1469 pub async fn move_storage(&self, new_path: std::path::PathBuf) -> crate::Result<()> {
1478 let (tx, rx) = oneshot::channel();
1479 self.cmd_tx
1480 .send(TorrentCommand::MoveStorage {
1481 new_path,
1482 reply: tx,
1483 })
1484 .await
1485 .map_err(|_| crate::Error::Shutdown)?;
1486 rx.await.map_err(|_| crate::Error::Shutdown)?
1487 }
1488
1489 pub async fn set_download_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1495 let (tx, rx) = oneshot::channel();
1496 self.cmd_tx
1497 .send(TorrentCommand::SetDownloadLimit {
1498 bytes_per_sec,
1499 reply: tx,
1500 })
1501 .await
1502 .map_err(|_| crate::Error::Shutdown)?;
1503 rx.await.map_err(|_| crate::Error::Shutdown)
1504 }
1505
1506 pub async fn set_upload_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1512 let (tx, rx) = oneshot::channel();
1513 self.cmd_tx
1514 .send(TorrentCommand::SetUploadLimit {
1515 bytes_per_sec,
1516 reply: tx,
1517 })
1518 .await
1519 .map_err(|_| crate::Error::Shutdown)?;
1520 rx.await.map_err(|_| crate::Error::Shutdown)
1521 }
1522
1523 pub async fn download_limit(&self) -> crate::Result<u64> {
1529 let (tx, rx) = oneshot::channel();
1530 self.cmd_tx
1531 .send(TorrentCommand::DownloadLimit { reply: tx })
1532 .await
1533 .map_err(|_| crate::Error::Shutdown)?;
1534 rx.await.map_err(|_| crate::Error::Shutdown)
1535 }
1536
1537 pub async fn upload_limit(&self) -> crate::Result<u64> {
1543 let (tx, rx) = oneshot::channel();
1544 self.cmd_tx
1545 .send(TorrentCommand::UploadLimit { reply: tx })
1546 .await
1547 .map_err(|_| crate::Error::Shutdown)?;
1548 rx.await.map_err(|_| crate::Error::Shutdown)
1549 }
1550
1551 pub async fn set_sequential_download(&self, enabled: bool) -> crate::Result<()> {
1557 let (tx, rx) = oneshot::channel();
1558 self.cmd_tx
1559 .send(TorrentCommand::SetSequentialDownload { enabled, reply: tx })
1560 .await
1561 .map_err(|_| crate::Error::Shutdown)?;
1562 rx.await.map_err(|_| crate::Error::Shutdown)
1563 }
1564
1565 pub async fn is_sequential_download(&self) -> crate::Result<bool> {
1571 let (tx, rx) = oneshot::channel();
1572 self.cmd_tx
1573 .send(TorrentCommand::IsSequentialDownload { reply: tx })
1574 .await
1575 .map_err(|_| crate::Error::Shutdown)?;
1576 rx.await.map_err(|_| crate::Error::Shutdown)
1577 }
1578
1579 pub async fn set_prioritize_first_last_pieces(&self, enabled: bool) -> crate::Result<()> {
1585 let (tx, rx) = oneshot::channel();
1586 self.cmd_tx
1587 .send(TorrentCommand::SetPrioritizeFirstLastPieces { enabled, reply: tx })
1588 .await
1589 .map_err(|_| crate::Error::Shutdown)?;
1590 rx.await.map_err(|_| crate::Error::Shutdown)
1591 }
1592
1593 pub async fn is_prioritize_first_last_pieces(&self) -> crate::Result<bool> {
1599 let (tx, rx) = oneshot::channel();
1600 self.cmd_tx
1601 .send(TorrentCommand::IsPrioritizeFirstLastPieces { reply: tx })
1602 .await
1603 .map_err(|_| crate::Error::Shutdown)?;
1604 rx.await.map_err(|_| crate::Error::Shutdown)
1605 }
1606
1607 pub async fn set_super_seeding(&self, enabled: bool) -> crate::Result<()> {
1613 let (tx, rx) = oneshot::channel();
1614 self.cmd_tx
1615 .send(TorrentCommand::SetSuperSeeding { enabled, reply: tx })
1616 .await
1617 .map_err(|_| crate::Error::Shutdown)?;
1618 rx.await.map_err(|_| crate::Error::Shutdown)
1619 }
1620
1621 pub async fn is_super_seeding(&self) -> crate::Result<bool> {
1627 let (tx, rx) = oneshot::channel();
1628 self.cmd_tx
1629 .send(TorrentCommand::IsSuperSeeding { reply: tx })
1630 .await
1631 .map_err(|_| crate::Error::Shutdown)?;
1632 rx.await.map_err(|_| crate::Error::Shutdown)
1633 }
1634
1635 pub async fn set_seed_mode(&self, enabled: bool) -> crate::Result<()> {
1646 let (tx, rx) = oneshot::channel();
1647 self.cmd_tx
1648 .send(TorrentCommand::SetSeedMode { enabled, reply: tx })
1649 .await
1650 .map_err(|_| crate::Error::Shutdown)?;
1651 rx.await.map_err(|_| crate::Error::Shutdown)
1652 }
1653
1654 pub async fn add_tracker(&self, url: String) -> crate::Result<()> {
1662 self.cmd_tx
1663 .send(TorrentCommand::AddTracker { url })
1664 .await
1665 .map_err(|_| crate::Error::Shutdown)
1666 }
1667
1668 pub async fn replace_trackers(&self, urls: Vec<String>) -> crate::Result<()> {
1674 let (tx, rx) = oneshot::channel();
1675 self.cmd_tx
1676 .send(TorrentCommand::ReplaceTrackers { urls, reply: tx })
1677 .await
1678 .map_err(|_| crate::Error::Shutdown)?;
1679 rx.await.map_err(|_| crate::Error::Shutdown)
1680 }
1681
1682 pub async fn force_recheck(&self) -> crate::Result<()> {
1693 let (tx, rx) = oneshot::channel();
1694 self.cmd_tx
1695 .send(TorrentCommand::ForceRecheck { reply: tx })
1696 .await
1697 .map_err(|_| crate::Error::Shutdown)?;
1698 rx.await.map_err(|_| crate::Error::Shutdown)?
1699 }
1700
1701 pub async fn rename_file(&self, file_index: usize, new_name: String) -> crate::Result<()> {
1711 let (tx, rx) = oneshot::channel();
1712 self.cmd_tx
1713 .send(TorrentCommand::RenameFile {
1714 file_index,
1715 new_name,
1716 reply: tx,
1717 })
1718 .await
1719 .map_err(|_| crate::Error::Shutdown)?;
1720 rx.await.map_err(|_| crate::Error::Shutdown)?
1721 }
1722
1723 pub async fn spawn_ssl_peer(
1728 &self,
1729 addr: SocketAddr,
1730 stream: impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
1731 ) -> crate::Result<()> {
1732 self.cmd_tx
1733 .send(TorrentCommand::SpawnSslPeer {
1734 addr,
1735 stream: crate::types::BoxedAsyncStream(Box::new(stream)),
1736 })
1737 .await
1738 .map_err(|_| crate::Error::Shutdown)
1739 }
1740
1741 pub async fn set_max_connections(&self, limit: usize) -> crate::Result<()> {
1747 let (tx, rx) = oneshot::channel();
1748 self.cmd_tx
1749 .send(TorrentCommand::SetMaxConnections { limit, reply: tx })
1750 .await
1751 .map_err(|_| crate::Error::Shutdown)?;
1752 rx.await.map_err(|_| crate::Error::Shutdown)
1753 }
1754
1755 pub async fn max_connections(&self) -> crate::Result<usize> {
1761 let (tx, rx) = oneshot::channel();
1762 self.cmd_tx
1763 .send(TorrentCommand::MaxConnections { reply: tx })
1764 .await
1765 .map_err(|_| crate::Error::Shutdown)?;
1766 rx.await.map_err(|_| crate::Error::Shutdown)
1767 }
1768
1769 pub async fn set_max_uploads(&self, limit: usize) -> crate::Result<()> {
1775 let (tx, rx) = oneshot::channel();
1776 self.cmd_tx
1777 .send(TorrentCommand::SetMaxUploads { limit, reply: tx })
1778 .await
1779 .map_err(|_| crate::Error::Shutdown)?;
1780 rx.await.map_err(|_| crate::Error::Shutdown)
1781 }
1782
1783 pub async fn max_uploads(&self) -> crate::Result<usize> {
1789 let (tx, rx) = oneshot::channel();
1790 self.cmd_tx
1791 .send(TorrentCommand::MaxUploads { reply: tx })
1792 .await
1793 .map_err(|_| crate::Error::Shutdown)?;
1794 rx.await.map_err(|_| crate::Error::Shutdown)
1795 }
1796
1797 pub async fn get_peer_info(&self) -> crate::Result<Vec<PeerInfo>> {
1803 let (tx, rx) = oneshot::channel();
1804 self.cmd_tx
1805 .send(TorrentCommand::GetPeerInfo { reply: tx })
1806 .await
1807 .map_err(|_| crate::Error::Shutdown)?;
1808 rx.await.map_err(|_| crate::Error::Shutdown)
1809 }
1810
1811 pub async fn get_download_queue(&self) -> crate::Result<Vec<PartialPieceInfo>> {
1817 let (tx, rx) = oneshot::channel();
1818 self.cmd_tx
1819 .send(TorrentCommand::GetDownloadQueue { reply: tx })
1820 .await
1821 .map_err(|_| crate::Error::Shutdown)?;
1822 rx.await.map_err(|_| crate::Error::Shutdown)
1823 }
1824
1825 pub async fn have_piece(&self, index: u32) -> crate::Result<bool> {
1831 let (tx, rx) = oneshot::channel();
1832 self.cmd_tx
1833 .send(TorrentCommand::HavePiece { index, reply: tx })
1834 .await
1835 .map_err(|_| crate::Error::Shutdown)?;
1836 rx.await.map_err(|_| crate::Error::Shutdown)
1837 }
1838
1839 pub async fn piece_availability(&self) -> crate::Result<Vec<u32>> {
1845 let (tx, rx) = oneshot::channel();
1846 self.cmd_tx
1847 .send(TorrentCommand::PieceAvailability { reply: tx })
1848 .await
1849 .map_err(|_| crate::Error::Shutdown)?;
1850 rx.await.map_err(|_| crate::Error::Shutdown)
1851 }
1852
1853 pub async fn file_progress(&self) -> crate::Result<Vec<u64>> {
1859 let (tx, rx) = oneshot::channel();
1860 self.cmd_tx
1861 .send(TorrentCommand::FileProgress { reply: tx })
1862 .await
1863 .map_err(|_| crate::Error::Shutdown)?;
1864 rx.await.map_err(|_| crate::Error::Shutdown)
1865 }
1866
1867 pub async fn info_hashes(&self) -> crate::Result<irontide_core::InfoHashes> {
1873 let (tx, rx) = oneshot::channel();
1874 self.cmd_tx
1875 .send(TorrentCommand::InfoHashes { reply: tx })
1876 .await
1877 .map_err(|_| crate::Error::Shutdown)?;
1878 rx.await.map_err(|_| crate::Error::Shutdown)
1879 }
1880
1881 pub async fn torrent_file(&self) -> crate::Result<Option<TorrentMetaV1>> {
1889 let (tx, rx) = oneshot::channel();
1890 self.cmd_tx
1891 .send(TorrentCommand::TorrentFile { reply: tx })
1892 .await
1893 .map_err(|_| crate::Error::Shutdown)?;
1894 rx.await.map_err(|_| crate::Error::Shutdown)
1895 }
1896
1897 pub async fn torrent_file_v2(&self) -> crate::Result<Option<irontide_core::TorrentMetaV2>> {
1906 let (tx, rx) = oneshot::channel();
1907 self.cmd_tx
1908 .send(TorrentCommand::TorrentFileV2 { reply: tx })
1909 .await
1910 .map_err(|_| crate::Error::Shutdown)?;
1911 rx.await.map_err(|_| crate::Error::Shutdown)
1912 }
1913
1914 pub async fn force_dht_announce(&self) -> crate::Result<()> {
1922 self.cmd_tx
1923 .send(TorrentCommand::ForceDhtAnnounce)
1924 .await
1925 .map_err(|_| crate::Error::Shutdown)
1926 }
1927
1928 pub async fn read_piece(&self, index: u32) -> crate::Result<Bytes> {
1937 let (tx, rx) = oneshot::channel();
1938 self.cmd_tx
1939 .send(TorrentCommand::ReadPiece { index, reply: tx })
1940 .await
1941 .map_err(|_| crate::Error::Shutdown)?;
1942 rx.await.map_err(|_| crate::Error::Shutdown)?
1943 }
1944
1945 pub async fn flush_cache(&self) -> crate::Result<()> {
1951 let (tx, rx) = oneshot::channel();
1952 self.cmd_tx
1953 .send(TorrentCommand::FlushCache { reply: tx })
1954 .await
1955 .map_err(|_| crate::Error::Shutdown)?;
1956 rx.await.map_err(|_| crate::Error::Shutdown)?
1957 }
1958
1959 #[must_use]
1964 pub fn is_valid(&self) -> bool {
1965 !self.cmd_tx.is_closed()
1966 }
1967
1968 pub async fn clear_error(&self) -> crate::Result<()> {
1974 self.cmd_tx
1975 .send(TorrentCommand::ClearError)
1976 .await
1977 .map_err(|_| crate::Error::Shutdown)
1978 }
1979
1980 pub async fn file_status(&self) -> crate::Result<Vec<crate::types::FileStatus>> {
1988 let (tx, rx) = oneshot::channel();
1989 self.cmd_tx
1990 .send(TorrentCommand::FileStatus { reply: tx })
1991 .await
1992 .map_err(|_| crate::Error::Shutdown)?;
1993 rx.await.map_err(|_| crate::Error::Shutdown)
1994 }
1995
1996 pub async fn flags(&self) -> crate::Result<crate::types::TorrentFlags> {
2002 let (tx, rx) = oneshot::channel();
2003 self.cmd_tx
2004 .send(TorrentCommand::Flags { reply: tx })
2005 .await
2006 .map_err(|_| crate::Error::Shutdown)?;
2007 rx.await.map_err(|_| crate::Error::Shutdown)
2008 }
2009
2010 pub async fn set_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
2018 let (tx, rx) = oneshot::channel();
2019 self.cmd_tx
2020 .send(TorrentCommand::SetFlags { flags, reply: tx })
2021 .await
2022 .map_err(|_| crate::Error::Shutdown)?;
2023 rx.await.map_err(|_| crate::Error::Shutdown)
2024 }
2025
2026 pub async fn unset_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
2034 let (tx, rx) = oneshot::channel();
2035 self.cmd_tx
2036 .send(TorrentCommand::UnsetFlags { flags, reply: tx })
2037 .await
2038 .map_err(|_| crate::Error::Shutdown)?;
2039 rx.await.map_err(|_| crate::Error::Shutdown)
2040 }
2041
2042 pub async fn connect_peer(&self, addr: SocketAddr) -> crate::Result<()> {
2051 self.cmd_tx
2052 .send(TorrentCommand::ConnectPeer { addr })
2053 .await
2054 .map_err(|_| crate::Error::Shutdown)
2055 }
2056
2057 pub fn send_pre_resolved_metadata(&self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
2063 let _ = self
2064 .cmd_tx
2065 .try_send(TorrentCommand::PreResolvedMetadata { info_bytes, peers });
2066 }
2067
2068 #[cfg(feature = "test-util")]
2087 pub async fn test_inject_metadata(&self, info_bytes: Vec<u8>) -> crate::Result<()> {
2088 let (tx, rx) = tokio::sync::oneshot::channel();
2089 self.cmd_tx
2090 .send(TorrentCommand::TestInjectMetadata {
2091 info_bytes,
2092 reply: tx,
2093 })
2094 .await
2095 .map_err(|_| crate::Error::Shutdown)?;
2096 rx.await.map_err(|_| crate::Error::Shutdown)?;
2097 Ok(())
2098 }
2099}
2100
2101#[derive(Debug, Clone)]
2107pub(crate) struct CachedFileEntry {
2108 pub(crate) index: usize,
2109 #[allow(dead_code)] pub(crate) length: u64,
2111 pub(crate) first_piece: u32,
2112 pub(crate) last_piece: u32,
2113}
2114
2115#[derive(Debug, Clone)]
2117pub(crate) struct CachedFileInfo {
2118 pub(crate) entries: Vec<CachedFileEntry>,
2119}
2120
2121pub(crate) fn build_cached_file_info(meta: &TorrentMetaV1, lengths: &Lengths) -> CachedFileInfo {
2122 let piece_length = lengths.piece_length();
2123 let files = meta.info.files();
2124 let mut entries = Vec::with_capacity(files.len());
2125 let mut offset = 0u64;
2126 for (index, file) in files.iter().enumerate() {
2127 let first_piece = (offset / piece_length) as u32;
2128 let last_piece = if file.length == 0 {
2129 first_piece
2130 } else {
2131 ((offset + file.length - 1) / piece_length) as u32
2132 };
2133 entries.push(CachedFileEntry {
2134 index,
2135 length: file.length,
2136 first_piece,
2137 last_piece,
2138 });
2139 offset += file.length;
2140 }
2141 CachedFileInfo { entries }
2142}
2143
2144pub(crate) struct TorrentActor {
2149 pub(crate) config: TorrentConfig,
2150 pub(crate) lock_timing: crate::timed_lock::LockTimingSettings,
2152 pub(crate) info_hash: Id20,
2153 pub(crate) our_peer_id: Id20,
2154 pub(crate) state: TorrentState,
2155
2156 pub(crate) disk: Option<DiskHandle>,
2158 pub(crate) disk_manager: DiskManagerHandle,
2159 pub(crate) chunk_tracker: Option<ChunkTracker>,
2160 pub(crate) lengths: Option<Lengths>,
2161 pub(crate) num_pieces: u32,
2162
2163 pub(crate) file_priorities: Vec<FilePriority>,
2165 pub(crate) wanted_pieces: Bitfield,
2166 pub(crate) end_game: EndGame,
2167
2168 pub(crate) streaming_pieces: BTreeSet<u32>,
2170 pub(crate) time_critical_pieces: BTreeSet<u32>,
2171 pub(crate) streaming_cursors: Vec<crate::streaming::StreamingCursor>,
2172 pub(crate) piece_ready_tx: broadcast::Sender<u32>,
2173 pub(crate) have_watch_tx: tokio::sync::watch::Sender<Bitfield>,
2174 pub(crate) have_watch_rx: tokio::sync::watch::Receiver<Bitfield>,
2175 pub(crate) stream_read_semaphore: Arc<tokio::sync::Semaphore>,
2176
2177 pub(crate) peers: HashMap<SocketAddr, PeerState>,
2179 pub(crate) unchoke_durations: HashMap<SocketAddr, Duration>,
2188 pub(crate) cached_peer_rates: FxHashMap<SocketAddr, f64>,
2191 #[allow(dead_code)]
2193 pub(crate) refill_notify: Arc<tokio::sync::Notify>,
2194 pub(crate) atomic_states: Option<Arc<crate::piece_reservation::AtomicPieceStates>>,
2196 pub(crate) block_maps: Option<Arc<BlockMaps>>,
2198 pub(crate) steal_candidates: Option<Arc<StealCandidates>>,
2200 pub(crate) last_steal_populate: Instant,
2202 pub(crate) piece_write_guards: Option<Arc<crate::piece_reservation::PieceWriteGuards>>,
2204 pub(crate) soft_reap_buf: Vec<std::net::SocketAddr>,
2208 pub(crate) eviction_history: std::collections::VecDeque<std::time::Instant>,
2213 pub(crate) force_immediate_choker_tick: bool,
2218 pub(crate) piece_tracker: Option<PieceTracker>,
2220 pub(crate) order_map_tx: tokio::sync::watch::Sender<Arc<PieceOrderMap>>,
2222
2223 pub(crate) order_map_dirty: bool,
2228
2229 pub(crate) next_order_map_gen: u64,
2235 pub(crate) piece_owner: Vec<Option<u16>>,
2237 pub(crate) peer_slab: crate::piece_reservation::PeerSlab,
2239 #[allow(dead_code)]
2240 pub(crate) priority_pieces: BTreeSet<u32>,
2241 pub(crate) max_in_flight: usize,
2243 pub(crate) reservation_notify: Option<Arc<tokio::sync::Notify>>,
2245 pub(crate) last_tick_dispatch_state: Option<(u32, usize)>,
2253 pub(crate) choker: Choker,
2254 pub(crate) user_seed_mode: bool,
2261 pub(crate) user_forced: bool,
2263 pub(crate) max_connections: usize,
2265 pub(crate) peer_states: Option<Arc<crate::peer_states::PeerStates>>,
2267 pub(crate) connect_semaphore: Arc<tokio::sync::Semaphore>,
2270 pub(crate) connect_permits:
2273 HashMap<SocketAddr, Arc<parking_lot::Mutex<Option<tokio::sync::OwnedSemaphorePermit>>>>,
2274 pub(crate) connect_rx: Option<mpsc::Receiver<ConnectPeer>>,
2276
2277 pub(crate) metadata_downloader: Option<MetadataDownloader>,
2279
2280 pub(crate) meta: Option<TorrentMetaV1>,
2282
2283 pub(crate) cached_files: Option<CachedFileInfo>,
2285
2286 pub(crate) downloaded: u64,
2288 pub(crate) uploaded: u64,
2289 pub(crate) checking_progress: f32,
2290 pub(crate) total_download: u64,
2291 pub(crate) total_upload: u64,
2292 pub(crate) total_failed_bytes: u64,
2293 pub(crate) total_redundant_bytes: u64,
2294 pub(crate) added_time: i64,
2295 pub(crate) completed_time: i64,
2296 pub(crate) last_download: i64,
2297 pub(crate) last_upload: i64,
2298 pub(crate) last_seen_complete: i64,
2299 pub(crate) active_duration: i64,
2300 pub(crate) finished_duration: i64,
2301 pub(crate) seeding_duration: i64,
2302 pub(crate) active_since: Option<std::time::Instant>,
2303 pub(crate) state_duration_since: Option<std::time::Instant>,
2304 #[allow(dead_code)] pub(crate) started_at: std::time::Instant,
2306 pub(crate) moving_storage: bool,
2307 pub(crate) has_incoming: bool,
2308 pub(crate) need_save_resume: bool,
2309 pub(crate) error: String,
2310 pub(crate) error_file: i32,
2311
2312 pub(crate) cmd_rx: mpsc::Receiver<TorrentCommand>,
2314 pub(crate) event_tx: mpsc::Sender<PeerEvent>,
2315 pub(crate) event_rx: mpsc::Receiver<PeerEvent>,
2316
2317 pub(crate) write_error_rx: mpsc::Receiver<crate::disk::DiskWriteError>,
2319 pub(crate) write_error_tx: mpsc::Sender<crate::disk::DiskWriteError>,
2320 pub(crate) verify_result_rx: mpsc::Receiver<crate::disk::VerifyResult>,
2321 pub(crate) verify_result_tx: mpsc::Sender<crate::disk::VerifyResult>,
2322 pub(crate) pending_verify: HashSet<u32>,
2325 pub(crate) piece_generations: Vec<u64>,
2328 pub(crate) hash_result_rx: tokio::sync::mpsc::Receiver<crate::hash_pool::HashResult>,
2330 pub(crate) hash_result_tx: tokio::sync::mpsc::Sender<crate::hash_pool::HashResult>,
2332
2333 pub(crate) listener: Option<Box<dyn crate::transport::TransportListener>>,
2335
2336 pub(crate) utp_socket: Option<irontide_utp::UtpSocket>,
2338 pub(crate) utp_socket_v6: Option<irontide_utp::UtpSocket>,
2340
2341 pub(crate) tracker_manager: TrackerManager,
2343 pub(crate) tracker_result_rx: Option<mpsc::Receiver<crate::tracker_manager::TrackerPeerBatch>>,
2346
2347 pub(crate) dht_rx: irontide_dht::DhtReceiver,
2354 pub(crate) dht_v6_rx: irontide_dht::DhtReceiver,
2355 pub(crate) dht_enabled: bool,
2356 pub(crate) dht_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2357 pub(crate) dht_v6_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2358 pub(crate) dht_v6_empty_count: u32,
2361 pub(crate) dht_v6_last_retry: Option<std::time::Instant>,
2363
2364 pub(crate) alert_tx: broadcast::Sender<Alert>,
2366 pub(crate) alert_mask: Arc<AtomicU32>,
2367
2368 pub(crate) upload_bucket: crate::rate_limiter::TokenBucket,
2370 pub(crate) download_bucket: SharedBucket,
2371 pub(crate) global_upload_bucket: Option<SharedBucket>,
2372 #[allow(dead_code)] pub(crate) global_download_bucket: Option<SharedBucket>,
2374 pub(crate) slot_tuner: crate::slot_tuner::SlotTuner,
2375 pub(crate) upload_bytes_interval: u64,
2376
2377 pub(crate) peak_download_rate: u64,
2379
2380 pub(crate) rechoke_per_min_est: f64,
2385
2386 pub(crate) web_seeds: HashMap<String, mpsc::Sender<crate::web_seed::WebSeedCommand>>,
2388 pub(crate) banned_web_seeds: HashSet<String>,
2389 pub(crate) web_seed_in_flight: HashMap<u32, String>,
2390 pub(crate) web_seed_stats: HashMap<String, irontide_core::WebSeedStats>,
2394 pub(crate) pex_peer_count: usize,
2400 pub(crate) lsd_peer_count: usize,
2404
2405 pub(crate) super_seed: Option<crate::super_seed::SuperSeedState>,
2407 pub(crate) have_broadcast_tx: tokio::sync::broadcast::Sender<u32>,
2409
2410 pub(crate) suggested_to_peers: HashMap<SocketAddr, HashSet<u32>>,
2412
2413 pub(crate) predictive_have_sent: HashSet<u32>,
2415
2416 pub(crate) ban_manager: irontide_session_types::SharedBanManager,
2418 pub(crate) piece_contributors: HashMap<u32, HashSet<std::net::IpAddr>>,
2419 pub(crate) parole_pieces: HashMap<u32, crate::ban::ParoleState>,
2420
2421 pub(crate) ip_filter: irontide_session_types::SharedIpFilter,
2423
2424 pub(crate) external_ip: Option<std::net::IpAddr>,
2426
2427 pub(crate) share_lru: std::collections::VecDeque<u32>,
2431 pub(crate) share_max_pieces: usize,
2433
2434 pub(crate) plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
2436
2437 pub(crate) hash_picker: Option<irontide_core::HashPicker>,
2439 pub(crate) version: irontide_core::TorrentVersion,
2440 #[allow(dead_code)] pub(crate) meta_v2: Option<irontide_core::TorrentMetaV2>,
2442
2443 pub(crate) info_hashes: irontide_core::InfoHashes,
2445
2446 pub(crate) dht_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2448 pub(crate) dht_v6_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
2449
2450 pub(crate) magnet_selected_files: Option<Vec<irontide_core::FileSelection>>,
2453
2454 pub(crate) sam_session: Option<Arc<crate::i2p::SamSession>>,
2456
2457 pub(crate) i2p_accept_rx: Option<mpsc::Receiver<crate::i2p::SamStream>>,
2459
2460 pub(crate) i2p_peer_counter: u32,
2462
2463 pub(crate) i2p_destinations: HashMap<SocketAddr, crate::i2p::I2pDestination>,
2465
2466 pub(crate) ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
2468
2469 pub(crate) rate_limiter_set: crate::rate_limiter::RateLimiterSet,
2471 pub(crate) auto_sequential_active: bool,
2473 pub(crate) factory: Arc<crate::transport::NetworkFactory>,
2475 pub(crate) hash_pool_ref: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
2477 pub(crate) live_outgoing_peers:
2479 std::sync::Arc<parking_lot::RwLock<std::collections::HashMap<SocketAddr, u8>>>,
2480 pub(crate) connect_attempts: u64,
2482 pub(crate) connect_failures: u64,
2484 pub(crate) choke_rotations: u64,
2486 pub(crate) inflight_started: Vec<Option<Instant>>,
2489 pub(crate) completed_piece_times: std::collections::VecDeque<Duration>,
2491 pub(crate) piece_steals: u64,
2493 pub(crate) holepunch_relayed: u64,
2495 pub(crate) holepunch_relay_rate: HashMap<SocketAddr, (Instant, u32)>,
2497 pub(crate) holepunch_cooldowns: HashMap<SocketAddr, Instant>,
2499 pub(crate) holepunch_pending: Vec<SocketAddr>,
2501 pub(crate) counters: Arc<crate::stats::SessionCounters>,
2505}
2506
2507pub(crate) const END_GAME_DEPTH: usize = 128;
2514
2515impl TorrentActor {
2518 pub(crate) fn current_dht(&self) -> Option<irontide_dht::DhtHandle> {
2524 if self.dht_enabled {
2525 self.dht_rx.current()
2526 } else {
2527 None
2528 }
2529 }
2530
2531 pub(crate) fn current_dht_v6(&self) -> Option<irontide_dht::DhtHandle> {
2534 if self.dht_enabled {
2535 self.dht_v6_rx.current()
2536 } else {
2537 None
2538 }
2539 }
2540
2541 #[allow(dead_code)] pub(crate) async fn current_dht_or_wait(
2557 &mut self,
2558 hold: std::time::Duration,
2559 ) -> Option<irontide_dht::DhtHandle> {
2560 if !self.dht_enabled {
2561 return None;
2562 }
2563 if let Some(handle) = self.dht_rx.current() {
2564 return Some(handle);
2565 }
2566 match tokio::time::timeout(hold, self.dht_rx.changed()).await {
2568 Ok(Ok(())) => self.dht_rx.current(),
2569 Ok(Err(_)) | Err(_) => None,
2570 }
2571 }
2572
2573 async fn run(mut self) {
2575 self.verify_existing_pieces().await;
2577
2578 if let Some(ct) = &self.chunk_tracker {
2581 let atomic_states = Arc::new(AtomicPieceStates::new(
2582 self.num_pieces,
2583 ct.bitfield(),
2584 &self.wanted_pieces,
2585 ));
2586 self.atomic_states = Some(Arc::clone(&atomic_states));
2587 self.piece_owner = vec![None; self.num_pieces as usize];
2588 self.inflight_started = vec![None; self.num_pieces as usize];
2590 self.max_in_flight = self.config.max_in_flight_pieces;
2591
2592 if self.config.use_block_stealing {
2594 if let Some(ref lengths) = self.lengths {
2595 self.block_maps = Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
2596 }
2597 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
2598 }
2599 self.piece_write_guards = Some(Arc::new(
2601 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
2602 ));
2603
2604 self.piece_tracker = Some(PieceTracker::new(
2606 self.num_pieces,
2607 ct.bitfield(),
2608 &self.wanted_pieces,
2609 ));
2610 if let Some(ref cached) = self.cached_files {
2611 let file_piece_ranges: Vec<(u32, u32)> = cached
2612 .entries
2613 .iter()
2614 .map(|e| (e.first_piece, e.last_piece))
2615 .collect();
2616 let om = Arc::new(PieceOrderMap::build(
2617 &self.file_priorities,
2618 &file_piece_ranges,
2619 self.num_pieces,
2620 0,
2621 self.piece_ordering(),
2622 ));
2623 self.order_map_tx.send_replace(om);
2624 }
2625
2626 let notify = Arc::new(tokio::sync::Notify::new());
2627 self.reservation_notify = Some(notify);
2628 }
2629
2630 if self.state != TorrentState::Seeding {
2632 self.spawn_web_seeds();
2633 self.assign_pieces_to_web_seeds();
2634 }
2635
2636 let connect_semaphore = Arc::new(tokio::sync::Semaphore::new(
2639 self.effective_max_connections(),
2640 ));
2641 self.connect_semaphore = Arc::clone(&connect_semaphore);
2642 self.connect_permits.clear();
2643 let (queue_tx, queue_rx) = mpsc::unbounded_channel();
2647 let peer_states = Arc::new(crate::peer_states::PeerStates::new_with_config(
2648 queue_tx,
2649 self.config.eviction_ban_set_cap,
2650 std::time::Duration::from_secs(self.config.eviction_ban_duration_secs),
2651 ));
2652 self.peer_states = Some(Arc::clone(&peer_states));
2653 let (adder_connect_tx, adder_connect_rx) = mpsc::channel(64);
2654 self.connect_rx = Some(adder_connect_rx);
2655 tokio::spawn(peer_adder::peer_adder_task(
2657 queue_rx,
2658 Arc::clone(&connect_semaphore),
2659 Arc::clone(&peer_states),
2660 Arc::clone(&self.ban_manager),
2661 Arc::clone(&self.ip_filter),
2662 adder_connect_tx,
2663 ));
2664
2665 let mut unchoke_interval = tokio::time::interval(Duration::from_secs(10));
2666 let mut rate_interval = tokio::time::interval(Duration::from_secs(2));
2667 rate_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2668 let mut optimistic_interval = tokio::time::interval(Duration::from_secs(30));
2669 let mut refill_interval = tokio::time::interval(Duration::from_millis(100));
2670 let mut dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::ZERO));
2671 let mut suggest_interval = if self.config.suggest_mode {
2672 Some(tokio::time::interval(Duration::from_secs(30)))
2673 } else {
2674 None
2675 };
2676 let mut turnover_interval = tokio::time::interval(Duration::from_secs(1));
2678 let mut pipeline_tick_interval = tokio::time::interval(Duration::from_secs(1));
2679 pipeline_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2681 let mut end_game_tick_interval = tokio::time::interval(Duration::from_millis(200));
2682 end_game_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2683 let mut diag_interval = tokio::time::interval(Duration::from_secs(5));
2684 let mut conn_stats_interval = tokio::time::interval(Duration::from_secs(30));
2686 let mut metadata_timeout_interval = tokio::time::interval(Duration::from_secs(5));
2688 let mut soft_reap_interval = tokio::time::interval(Duration::from_secs(1));
2691 let mut eviction_interval = tokio::time::interval(Duration::from_secs(2));
2694 eviction_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
2695
2696 unchoke_interval.tick().await;
2698 optimistic_interval.tick().await;
2699 refill_interval.tick().await;
2700 if let Some(ref mut si) = suggest_interval {
2702 si.tick().await; }
2704 turnover_interval.tick().await;
2705 pipeline_tick_interval.tick().await;
2706 end_game_tick_interval.tick().await;
2707 diag_interval.tick().await;
2708 conn_stats_interval.tick().await;
2709 metadata_timeout_interval.tick().await;
2710 soft_reap_interval.tick().await;
2711 eviction_interval.tick().await;
2712
2713 if self.state == TorrentState::Downloading && self.config.enable_dht {
2716 if let Some(dht) = self.current_dht()
2718 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
2719 {
2720 warn!("DHT v4 announce failed: {e}");
2721 }
2722 if let Some(dht6) = self.current_dht_v6()
2723 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
2724 {
2725 debug!("DHT v6 announce failed: {e}");
2726 }
2727 if self.info_hashes.is_hybrid()
2729 && let Some(v2) = self.info_hashes.v2
2730 {
2731 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
2732 if v2_as_v1 != self.info_hash {
2733 if let Some(dht) = self.current_dht()
2734 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
2735 {
2736 debug!("DHT v4 dual-swarm announce failed: {e}");
2737 }
2738 if let Some(dht6) = self.current_dht_v6()
2739 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
2740 {
2741 debug!("DHT v6 dual-swarm announce failed: {e}");
2742 }
2743 }
2744 }
2745 }
2746
2747 if self.config.enable_i2p
2750 && let Some(ref sam) = self.sam_session
2751 {
2752 let (tx, rx) = mpsc::channel(16);
2753 let sam = Arc::clone(sam);
2754 tokio::spawn(async move {
2755 loop {
2756 match sam.accept().await {
2757 Ok(stream) => {
2758 if tx.send(stream).await.is_err() {
2759 break; }
2761 }
2762 Err(e) => {
2763 warn!("I2P accept error: {e}");
2764 tokio::time::sleep(Duration::from_secs(5)).await;
2765 }
2766 }
2767 }
2768 });
2769 self.i2p_accept_rx = Some(rx);
2770 }
2771
2772 loop {
2773 tokio::select! {
2774 biased;
2775 event = self.event_rx.recv() => {
2780 if let Some(event) = event {
2781 Self::ping_event_drain(&self.peers, &event);
2789 self.handle_peer_event(event)
2790 .instrument(tracing::debug_span!("handle_peer_event"))
2791 .await;
2792 for _ in 0..512 {
2794 match self.event_rx.try_recv() {
2795 Ok(event) => {
2796 Self::ping_event_drain(&self.peers, &event);
2797 self.handle_peer_event(event).await;
2798 }
2799 Err(_) => break,
2800 }
2801 }
2802 }
2803 }
2804 Some(result) = self.verify_result_rx.recv() => {
2806 self.pending_verify.remove(&result.piece);
2807 let dominated = self.chunk_tracker.as_ref()
2809 .is_some_and(|ct| ct.bitfield().get(result.piece));
2810 if !dominated {
2811 if result.passed {
2812 self.on_piece_verified(result.piece).await;
2813 } else {
2814 self.on_piece_hash_failed(result.piece).await;
2815 }
2817 }
2818 }
2819 Some(result) = self.hash_result_rx.recv() => {
2821 self.handle_hash_result(result).await;
2822 }
2823 cmd = self.cmd_rx.recv() => {
2825 match cmd {
2826 Some(TorrentCommand::AddPeers { peers, source }) => {
2827 self.handle_add_peers(peers, source);
2828 }
2829 Some(TorrentCommand::Stats { reply }) => {
2830 let _ = reply.send(self.make_stats());
2831 }
2832 Some(TorrentCommand::Pause) => {
2833 self.handle_pause().await;
2834 }
2835 Some(TorrentCommand::Queue) => {
2836 self.handle_queue();
2837 }
2838 Some(TorrentCommand::Resume) => {
2839 self.handle_resume().await;
2840 }
2841 Some(TorrentCommand::ForceResume) => {
2842 self.user_forced = true;
2843 self.handle_resume().await;
2844 }
2845 Some(TorrentCommand::SetCategory { category, reply }) => {
2846 self.config.category = category;
2850 self.need_save_resume = true;
2851 let _ = reply.send(());
2852 }
2853 Some(TorrentCommand::SetTags { tags, reply }) => {
2854 self.config.tags = tags;
2861 self.need_save_resume = true;
2862 let _ = reply.send(());
2863 }
2864 Some(TorrentCommand::GetWebSeeds { reply }) => {
2865 let urls = match &self.meta {
2871 Some(meta) => {
2872 let mut v = Vec::with_capacity(
2873 meta.url_list.len() + meta.httpseeds.len(),
2874 );
2875 v.extend(meta.url_list.iter().cloned());
2876 v.extend(meta.httpseeds.iter().cloned());
2877 v
2878 }
2879 None => Vec::new(),
2880 };
2881 let _ = reply.send(urls);
2882 }
2883 Some(TorrentCommand::GetPieceStates { reply }) => {
2884 let states = match self.atomic_states.as_ref() {
2888 Some(atomic) => atomic.snapshot(),
2889 None => Vec::new(),
2890 };
2891 let _ = reply.send(states);
2892 }
2893 Some(TorrentCommand::GetPieceHashes { offset, limit, reply }) => {
2894 let offset = offset as usize;
2911 let limit = limit as usize;
2912 let raw: Vec<Vec<u8>> = match self.version {
2913 irontide_core::TorrentVersion::V1Only
2914 | irontide_core::TorrentVersion::Hybrid => self
2915 .meta
2916 .as_ref()
2917 .map(|meta| {
2918 meta.info
2919 .pieces
2920 .chunks_exact(20)
2921 .skip(offset)
2922 .take(limit)
2923 .map(<[u8]>::to_vec)
2924 .collect::<Vec<Vec<u8>>>()
2925 })
2926 .unwrap_or_default(),
2927 irontide_core::TorrentVersion::V2Only => self
2928 .meta_v2
2929 .as_ref()
2930 .map(|m| {
2931 m.piece_layers
2932 .values()
2933 .flat_map(|v| v.chunks_exact(32))
2934 .skip(offset)
2935 .take(limit)
2936 .map(<[u8]>::to_vec)
2937 .collect::<Vec<Vec<u8>>>()
2938 })
2939 .unwrap_or_default(),
2940 };
2941 let _ = reply.send(raw);
2942 }
2943 Some(TorrentCommand::SaveResumeData { reply }) => {
2944 let result = self.build_resume_data();
2945 let _ = reply.send(result);
2946 }
2947 Some(TorrentCommand::TakeResumeIfDirty { reply }) => {
2948 let result = if self.need_save_resume {
2962 let built = self.build_resume_data();
2963 if built.is_ok() {
2964 self.need_save_resume = false;
2965 }
2966 built.map(Some)
2967 } else {
2968 Ok(None)
2969 };
2970 let _ = reply.send(result);
2971 }
2972 Some(TorrentCommand::SetFilePriority { index, priority, reply }) => {
2973 match self.apply_file_priority_scoped(index, priority) {
2983 Ok((first, last)) => {
2984 self.sync_piece_states_for_range(first, last);
2985 if let Some(ref mut pt) = self.piece_tracker {
2986 for piece in first..=last {
2987 if self.wanted_pieces.get(piece) {
2988 pt.mark_wanted(piece);
2989 } else {
2990 pt.mark_unwanted(piece);
2991 }
2992 }
2993 }
2994 self.order_map_dirty = true;
2995 let _ = reply.send(Ok(()));
2996 }
2997 Err(e) => {
2998 let _ = reply.send(Err(e));
2999 }
3000 }
3001 }
3002 Some(TorrentCommand::FilePriorities { reply }) => {
3003 let _ = reply.send(self.file_priorities.clone());
3004 }
3005 Some(TorrentCommand::ForceReannounce) => {
3006 self.tracker_manager.force_reannounce();
3007 }
3008 Some(TorrentCommand::TrackerList { reply }) => {
3009 let _ = reply.send(self.tracker_manager.tracker_list());
3010 }
3011 Some(TorrentCommand::Scrape { reply }) => {
3012 let result = self.tracker_manager.scrape().await;
3013 if let Some((ref url, ref info)) = result {
3014 post_alert(&self.alert_tx, &self.alert_mask, AlertKind::ScrapeReply {
3015 info_hash: self.info_hash,
3016 url: url.clone(),
3017 complete: info.complete,
3018 incomplete: info.incomplete,
3019 downloaded: info.downloaded,
3020 });
3021 }
3022 let _ = reply.send(result);
3023 }
3024 Some(TorrentCommand::OpenFile { file_index, reply }) => {
3025 let result = self.handle_open_file(file_index);
3026 let _ = reply.send(result);
3027 }
3028 Some(TorrentCommand::IncomingPeer { stream, addr }) => {
3029 self.spawn_peer_from_stream_with_mode(
3030 addr,
3031 stream,
3032 Some(irontide_wire::mse::EncryptionMode::Disabled),
3033 );
3034 }
3035 Some(TorrentCommand::UpdateExternalIp { ip }) => {
3036 self.external_ip = Some(ip);
3037 post_alert(
3038 &self.alert_tx,
3039 &self.alert_mask,
3040 AlertKind::ExternalIpDetected { ip },
3041 );
3042 }
3043 Some(TorrentCommand::MoveStorage { new_path, reply }) => {
3044 let result = self.handle_move_storage(new_path).await;
3045 let _ = reply.send(result);
3046 }
3047 Some(TorrentCommand::SpawnSslPeer { addr, stream }) => {
3048 self.spawn_peer_from_stream_with_mode(
3050 addr,
3051 stream.0,
3052 Some(irontide_wire::mse::EncryptionMode::Disabled),
3053 );
3054 }
3055 Some(TorrentCommand::SetDownloadLimit { bytes_per_sec, reply }) => {
3056 self.download_bucket.lock().set_rate(bytes_per_sec);
3057 let _ = reply.send(());
3058 }
3059 Some(TorrentCommand::SetUploadLimit { bytes_per_sec, reply }) => {
3060 self.upload_bucket.set_rate(bytes_per_sec);
3061 let _ = reply.send(());
3062 }
3063 Some(TorrentCommand::DownloadLimit { reply }) => {
3064 let _ = reply.send(self.download_bucket.lock().rate());
3065 }
3066 Some(TorrentCommand::UploadLimit { reply }) => {
3067 let _ = reply.send(self.upload_bucket.rate());
3068 }
3069 Some(TorrentCommand::SetSequentialDownload { enabled, reply }) => {
3070 if self.config.sequential_download != enabled {
3071 self.config.sequential_download = enabled;
3072 self.order_map_dirty = true;
3075 }
3076 let _ = reply.send(());
3077 }
3078 Some(TorrentCommand::IsSequentialDownload { reply }) => {
3079 let _ = reply.send(self.config.sequential_download);
3080 }
3081 Some(TorrentCommand::SetPrioritizeFirstLastPieces { enabled, reply }) => {
3082 if self.config.prioritize_first_last_pieces != enabled {
3083 self.config.prioritize_first_last_pieces = enabled;
3084 self.order_map_dirty = true;
3085 }
3086 let _ = reply.send(());
3087 }
3088 Some(TorrentCommand::IsPrioritizeFirstLastPieces { reply }) => {
3089 let _ = reply.send(self.config.prioritize_first_last_pieces);
3090 }
3091 Some(TorrentCommand::SetSuperSeeding { enabled, reply }) => {
3092 self.config.super_seeding = enabled;
3093 self.super_seed = if enabled {
3094 Some(crate::super_seed::SuperSeedState::new())
3095 } else {
3096 None
3097 };
3098 let _ = reply.send(());
3099 }
3100 Some(TorrentCommand::IsSuperSeeding { reply }) => {
3101 let _ = reply.send(self.config.super_seeding);
3102 }
3103 Some(TorrentCommand::SetSeedMode { enabled, reply }) => {
3104 self.handle_set_seed_mode(enabled);
3105 let _ = reply.send(());
3106 }
3107 Some(TorrentCommand::SetSeedRatioLimit { limit, reply }) => {
3108 self.config.seed_ratio_limit = limit;
3109 self.need_save_resume = true;
3110 let _ = reply.send(());
3111 }
3112 Some(TorrentCommand::AddTracker { url }) => {
3113 self.tracker_manager.add_tracker_url(&url);
3114 }
3115 Some(TorrentCommand::ReplaceTrackers { urls, reply }) => {
3116 self.tracker_manager.replace_all(&urls);
3117 let _ = reply.send(());
3118 }
3119 Some(TorrentCommand::ForceRecheck { reply }) => {
3120 self.handle_force_recheck(reply).await;
3121 }
3122 Some(TorrentCommand::RenameFile { file_index, new_name, reply }) => {
3123 let result = self.handle_rename_file(file_index, new_name).await;
3124 let _ = reply.send(result);
3125 }
3126 Some(TorrentCommand::SetMaxConnections { limit, reply }) => {
3127 self.max_connections = limit;
3128 let _ = reply.send(());
3129 }
3130 Some(TorrentCommand::MaxConnections { reply }) => {
3131 let _ = reply.send(self.max_connections);
3132 }
3133 Some(TorrentCommand::SetMaxUploads { limit, reply }) => {
3134 self.choker.set_unchoke_slots(limit);
3135 let _ = reply.send(());
3136 }
3137 Some(TorrentCommand::MaxUploads { reply }) => {
3138 let _ = reply.send(self.choker.unchoke_slots());
3139 }
3140 Some(TorrentCommand::GetPeerInfo { reply }) => {
3141 let _ = reply.send(self.build_peer_info());
3142 }
3143 Some(TorrentCommand::GetDownloadQueue { reply }) => {
3144 let _ = reply.send(self.build_download_queue());
3145 }
3146 Some(TorrentCommand::HavePiece { index, reply }) => {
3147 let has = self.chunk_tracker.as_ref()
3148 .is_some_and(|ct| ct.has_piece(index));
3149 let _ = reply.send(has);
3150 }
3151 Some(TorrentCommand::PieceAvailability { reply }) => {
3152 let mut avail = vec![0u32; self.num_pieces as usize];
3163 for peer in self.peers.values() {
3164 for i in 0..self.num_pieces {
3165 if peer.bitfield.get(i) {
3166 avail[i as usize] += 1;
3167 }
3168 }
3169 }
3170 let _ = reply.send(avail);
3171 }
3172 Some(TorrentCommand::FileProgress { reply }) => {
3173 let _ = reply.send(self.compute_file_progress());
3174 }
3175 Some(TorrentCommand::InfoHashes { reply }) => {
3176 let _ = reply.send(self.info_hashes.clone());
3177 }
3178 Some(TorrentCommand::TorrentFile { reply }) => {
3179 let _ = reply.send(self.meta.clone());
3180 }
3181 Some(TorrentCommand::TorrentFileV2 { reply }) => {
3182 let _ = reply.send(self.meta_v2.clone());
3183 }
3184 Some(TorrentCommand::ForceDhtAnnounce) => {
3185 self.handle_force_dht_announce().await;
3186 }
3187 Some(TorrentCommand::ReadPiece { index, reply }) => {
3188 let result = self.handle_read_piece(index).await;
3189 let _ = reply.send(result);
3190 }
3191 Some(TorrentCommand::FlushCache { reply }) => {
3192 let result = self.handle_flush_cache().await;
3193 let _ = reply.send(result);
3194 }
3195 Some(TorrentCommand::ClearError) => {
3196 self.handle_clear_error().await;
3197 }
3198 Some(TorrentCommand::ClearSaveResumeFlag) => {
3199 self.need_save_resume = false;
3200 }
3201 Some(TorrentCommand::MarkResumeDirty) => {
3202 self.need_save_resume = true;
3206 }
3207 Some(TorrentCommand::RestoreResumeBitmap { pieces, reply }) => {
3208 let result = self.handle_restore_resume_bitmap(pieces);
3209 let _ = reply.send(result);
3210 }
3211 Some(TorrentCommand::RestoreWebSeedStats { stats, reply }) => {
3212 self.web_seed_stats = stats;
3213 let _ = reply.send(Ok(()));
3214 }
3215 Some(TorrentCommand::GetPeerSourceCounts { reply }) => {
3216 let _ = reply.send((self.pex_peer_count, self.lsd_peer_count));
3217 }
3218 Some(TorrentCommand::QueryUnchokeDurations { reply }) => {
3219 let mut out = self.unchoke_durations.clone();
3220 let now = Instant::now();
3223 for peer in self.peers.values() {
3224 let mut delta = peer.unchoke_duration_total;
3225 if let Some(start) = peer.am_unchoke_started_at {
3226 delta += now.duration_since(start);
3227 }
3228 if !delta.is_zero() {
3229 *out.entry(peer.addr).or_default() += delta;
3230 }
3231 }
3232 let _ = reply.send(out);
3233 }
3234 Some(TorrentCommand::GetWebSeedStats { reply }) => {
3235 let snapshot: Vec<_> = self.web_seed_stats.values().cloned().collect();
3236 let _ = reply.send(snapshot);
3237 }
3238 Some(TorrentCommand::FileStatus { reply }) => {
3239 let _ = reply.send(self.build_file_status());
3240 }
3241 Some(TorrentCommand::Flags { reply }) => {
3242 let _ = reply.send(self.build_flags());
3243 }
3244 Some(TorrentCommand::SetFlags { flags, reply }) => {
3245 self.apply_set_flags(flags).await;
3246 let _ = reply.send(());
3247 }
3248 Some(TorrentCommand::UnsetFlags { flags, reply }) => {
3249 self.apply_unset_flags(flags).await;
3250 let _ = reply.send(());
3251 }
3252 Some(TorrentCommand::ConnectPeer { addr }) => {
3253 self.handle_connect_peer(addr);
3254 }
3255 Some(TorrentCommand::PreResolvedMetadata { info_bytes, peers }) => {
3256 self.handle_pre_resolved_metadata(info_bytes, peers).await;
3257 }
3258 #[cfg(feature = "test-util")]
3259 Some(TorrentCommand::TestInjectMetadata { info_bytes, reply }) => {
3260 self.handle_pre_resolved_metadata(info_bytes, vec![]).await;
3264 let _ = reply.send(());
3265 }
3266 Some(TorrentCommand::GetMeta { reply }) => {
3267 let _ = reply.send(self.meta.clone());
3272 }
3273 Some(TorrentCommand::UpdateSettings(delta)) => {
3274 self.handle_update_settings(&delta);
3275 }
3276 Some(TorrentCommand::Shutdown) => {
3277 info!("torrent actor: received Shutdown command, exiting");
3278 self.shutdown_web_seeds().await;
3279 self.shutdown_peers().await;
3280 return;
3281 }
3282 None => {
3283 warn!("torrent actor: cmd_rx channel closed (all senders dropped), exiting");
3284 self.shutdown_web_seeds().await;
3285 self.shutdown_peers().await;
3286 return;
3287 }
3288 }
3289 }
3290 Some(err) = self.write_error_rx.recv() => {
3292 warn!(piece = err.piece, begin = err.begin, "async disk write failed: {}", err.error);
3293 }
3294 result = accept_incoming(&mut self.listener) => {
3296 if let Ok((stream, addr)) = result {
3297 self.spawn_peer_from_stream(addr, stream);
3298 }
3299 }
3300 stream = accept_i2p(&mut self.i2p_accept_rx) => {
3302 if let Some(stream) = stream {
3303 self.handle_i2p_incoming(stream);
3304 }
3305 }
3306 _ = rate_interval.tick() => {
3309 self.update_peer_rates();
3310 }
3311 _ = unchoke_interval.tick() => {
3313 if self.state == TorrentState::Seeding
3322 || self.state == TorrentState::Sharing
3323 {
3324 self.slot_tuner.observe(self.upload_bytes_interval);
3325 self.choker.observe_throughput(self.upload_bytes_interval);
3326 self.upload_bytes_interval = 0;
3327 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3328 self.run_choker().await;
3329 self.force_immediate_choker_tick = false;
3332 } else {
3333 self.upload_bytes_interval = 0;
3334 }
3335 self.update_streaming_cursors();
3337 if self.config.auto_sequential {
3339 let was = self.auto_sequential_active;
3340 self.auto_sequential_active = crate::piece_selector::evaluate_auto_sequential(
3341 self.piece_owner.iter().filter(|o| o.is_some()).count(),
3342 self.peers.len(),
3343 self.auto_sequential_active,
3344 );
3345 if was != self.auto_sequential_active {
3348 self.order_map_dirty = true;
3349 }
3350 }
3351 self.assign_pieces_to_web_seeds();
3353 }
3354 _ = optimistic_interval.tick() => {
3356 self.rotate_optimistic();
3357 }
3358 Some(connect_peer) = async {
3360 match self.connect_rx.as_mut() {
3361 Some(rx) => rx.recv().await,
3362 None => std::future::pending().await,
3363 }
3364 } => {
3365 self.handle_adder_connect(connect_peer);
3366 }
3367 () = &mut dht_requery_sleep, if self.state != TorrentState::Complete
3368 && self.state != TorrentState::Paused
3369 && self.state != TorrentState::Queued
3370 && self.state != TorrentState::Seeding
3371 && self.state != TorrentState::Stopped => {
3372 self.run_dht_requery().await;
3373 dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::from_mins(1)));
3374 }
3375 () = async {
3380 match self.tracker_manager.next_announce_in() {
3381 Some(dur) => tokio::time::sleep(dur).await,
3382 None => std::future::pending().await,
3383 }
3384 }, if self.tracker_result_rx.is_none() => {
3385 let left = self.calculate_left();
3386 self.tracker_result_rx = Some(self.tracker_manager.start_announce(
3387 irontide_tracker::AnnounceEvent::None,
3388 self.uploaded,
3389 self.downloaded,
3390 left,
3391 ));
3392 }
3393 result = async {
3396 match self.tracker_result_rx.as_mut() {
3397 Some(rx) => rx.recv().await,
3398 None => std::future::pending().await,
3399 }
3400 } => {
3401 match result {
3402 Some(batch) => {
3403 let (peers, outcome) = self.tracker_manager.process_tracker_result(batch);
3404 self.fire_tracker_alerts(&[outcome]);
3405 if !peers.is_empty() {
3406 debug!(count = peers.len(), "tracker returned peers (streaming)");
3407 self.handle_add_peers(peers, PeerSource::Tracker);
3408 }
3409 }
3410 None => {
3411 self.tracker_result_rx = None;
3414 }
3415 }
3416 }
3417 result = async {
3419 match &mut self.dht_peers_rx {
3420 Some(rx) => rx.recv().await,
3421 None => std::future::pending().await,
3422 }
3423 } => {
3424 if let Some(peers) = result {
3425 debug!(count = peers.len(), "DHT v4 returned peers");
3426 self.handle_add_peers(peers, PeerSource::Dht);
3427 } else {
3428 debug!("DHT v4 peer search exhausted");
3429 self.dht_peers_rx = None;
3430 }
3431 }
3432 result = async {
3434 match &mut self.dht_v6_peers_rx {
3435 Some(rx) => rx.recv().await,
3436 None => std::future::pending().await,
3437 }
3438 } => {
3439 if let Some(peers) = result {
3440 debug!(count = peers.len(), "DHT v6 returned peers");
3441 self.dht_v6_empty_count = 0; self.handle_add_peers(peers, PeerSource::Dht);
3443 } else {
3444 self.dht_v6_peers_rx = None;
3445 self.dht_v6_empty_count += 1;
3446 if self.dht_v6_empty_count == 30 {
3447 debug!("DHT v6 routing table persistently empty, giving up");
3448 } else if self.dht_v6_empty_count < 30 {
3449 debug!("DHT v6 peer search exhausted");
3450 }
3451 }
3452 }
3453 result = async {
3455 match &mut self.dht_v2_peers_rx {
3456 Some(rx) => rx.recv().await,
3457 None => std::future::pending().await,
3458 }
3459 } => {
3460 if let Some(peers) = result {
3461 debug!(count = peers.len(), "DHT v4 v2-swarm returned peers");
3462 self.handle_add_peers(peers, PeerSource::Dht);
3463 } else {
3464 debug!("DHT v4 v2-swarm peer search exhausted");
3465 self.dht_v2_peers_rx = None;
3466 }
3467 }
3468 result = async {
3470 match &mut self.dht_v6_v2_peers_rx {
3471 Some(rx) => rx.recv().await,
3472 None => std::future::pending().await,
3473 }
3474 } => {
3475 if let Some(peers) = result {
3476 debug!(count = peers.len(), "DHT v6 v2-swarm returned peers");
3477 self.handle_add_peers(peers, PeerSource::Dht);
3478 } else {
3479 debug!("DHT v6 v2-swarm peer search exhausted");
3480 self.dht_v6_v2_peers_rx = None;
3481 }
3482 }
3483 _ = async {
3485 match suggest_interval {
3486 Some(ref mut interval) => interval.tick().await,
3487 None => std::future::pending().await,
3488 }
3489 } => {
3490 self.suggest_cached_pieces().await;
3491 }
3492 _ = turnover_interval.tick() => {
3493 self.run_steal_queue_maintenance();
3494 }
3495 _ = pipeline_tick_interval.tick() => {
3497 let snub_timeout = Duration::from_secs(u64::from(self.config.snub_timeout_secs));
3498
3499 for (_addr, peer) in &mut self.peers {
3500 peer.pipeline.tick();
3501
3502 if !peer.peer_choking && !peer.snubbed {
3504 let idle = peer.last_data_received
3505 .is_some_and(|t| t.elapsed() > snub_timeout);
3506 if idle {
3507 peer.snubbed = true;
3508 peer.blocks_timed_out = peer.blocks_timed_out
3510 .saturating_add(peer.pending_requests.len() as u64);
3511 debug!(%_addr, "peer snubbed (no data for {}s)", self.config.snub_timeout_secs);
3512 }
3513 }
3514 }
3515
3516 self.refresh_peer_rates();
3519
3520 self.apply_request_budget();
3524
3525 if !self.end_game.is_active() {
3527 self.check_end_game_activation();
3528 }
3529
3530 self.tick_dispatch_safety_wake();
3531
3532 if self.config.choke_rotation_max_evictions > 0
3534 && self.state == TorrentState::Downloading
3535 {
3536 self.run_choke_rotation();
3537 }
3538
3539 if self.order_map_dirty {
3545 self.rebuild_order_map_now();
3546 }
3547 }
3548 _ = end_game_tick_interval.tick(), if self.end_game.is_active() => {
3553 let addrs: Vec<SocketAddr> = self.peers.iter()
3554 .filter(|(_, p)| !p.peer_choking && p.pending_requests.len() < END_GAME_DEPTH)
3555 .map(|(addr, _)| *addr)
3556 .collect();
3557 for addr in addrs {
3558 self.request_end_game_block(addr).await;
3559 }
3560 }
3561 _ = metadata_timeout_interval.tick(), if self.state == TorrentState::FetchingMetadata => {
3564 let timed_out: Vec<u32> = self
3566 .metadata_downloader
3567 .as_ref()
3568 .map(MetadataDownloader::timed_out_pieces)
3569 .unwrap_or_default();
3570
3571 if !timed_out.is_empty() {
3572 debug!(count = timed_out.len(), "metadata pieces timed out, re-requesting");
3573
3574 let eligible_senders: Vec<mpsc::Sender<PeerCommand>> = self
3577 .peers
3578 .iter()
3579 .filter(|(addr, peer)| {
3580 self.metadata_downloader
3581 .as_ref()
3582 .is_some_and(|dl| !dl.is_rejected(addr))
3583 && peer
3584 .ext_handshake
3585 .as_ref()
3586 .is_some_and(|h| h.metadata_size.is_some())
3587 })
3588 .map(|(_, peer)| peer.cmd_tx.clone())
3589 .collect();
3590
3591 for cmd_tx in &eligible_senders {
3593 for &piece in &timed_out {
3594 let _ = cmd_tx.try_send(PeerCommand::RequestMetadata { piece });
3595 }
3596 }
3597
3598 if let Some(ref mut dl) = self.metadata_downloader {
3600 for piece in timed_out {
3601 dl.reset_request_time(piece);
3602 }
3603 }
3604 }
3605 }
3606 _ = diag_interval.tick() => {
3608 {
3610 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3611 let eg = self.end_game.is_active();
3612 let eg_blocks = self.end_game.block_count();
3613 info!(state = ?self.state, have, total = self.num_pieces, end_game = eg, eg_blocks, "heartbeat");
3614 }
3615 if self.state == TorrentState::Downloading {
3616 let have = self.chunk_tracker.as_ref().map_or(0, |ct| ct.bitfield().count_ones());
3617 let in_flight = self.atomic_states.as_ref().map_or(0, |s| s.in_flight_count() as usize);
3618 let unchoked = self.peers.values().filter(|p| !p.peer_choking).count();
3619 info!(have, in_flight, total = self.num_pieces,
3620 downloaded_mb = self.downloaded / (1024 * 1024),
3621 peers = self.peers.len(), unchoked,
3622 "download progress");
3623 for (addr, p) in &self.peers {
3624 let last_data = p.last_data_received.map_or(9999, |t| t.elapsed().as_secs());
3625 trace!(%addr,
3626 choking = p.peer_choking,
3627 pending = p.pending_requests.len(),
3628 ewma_rate = p.pipeline.ewma_rate() as u64,
3629 last_data_secs = last_data,
3630 bf_ones = p.bitfield.count_ones(),
3631 "peer state");
3632 }
3633 }
3634 }
3635 _ = conn_stats_interval.tick() => {
3637 if self.connect_attempts > 0 {
3638 let succeeded = self.connect_attempts.saturating_sub(self.connect_failures);
3639 let success_pct = (succeeded as f64 / self.connect_attempts as f64 * 100.0) as u32;
3640 info!(
3641 connected = self.peers.len(),
3642 attempted = self.connect_attempts,
3643 failed = self.connect_failures,
3644 success_rate = %format!("{success_pct}%"),
3645 "connection stats"
3646 );
3647 }
3648 }
3649 _ = soft_reap_interval.tick() => {
3655 let soft_timeout = self.config.connect_soft_timeout;
3656 if soft_timeout > 0 {
3657 if let Some(ref ps) = self.peer_states {
3658 ps.soft_reap_candidates_into(
3659 Duration::from_secs(soft_timeout),
3660 &mut self.soft_reap_buf,
3661 );
3662 } else {
3663 self.soft_reap_buf.clear();
3664 }
3665 for i in 0..self.soft_reap_buf.len() {
3666 let peer_addr = self.soft_reap_buf[i];
3667 debug!(%peer_addr, soft_timeout, "soft reap: no TCP SYN-ACK");
3668 self.connect_permits.remove(&peer_addr);
3670 self.disconnect_peer(peer_addr, "soft reap: no TCP SYN-ACK");
3671 if let Some(ref ps) = self.peer_states
3672 && let Some(backoff) = ps.mark_dead(peer_addr)
3673 {
3674 let ps_clone = Arc::clone(ps);
3675 tokio::spawn(async move {
3676 tokio::time::sleep(backoff).await;
3677 ps_clone.mark_queued_for_retry(peer_addr);
3678 });
3679 }
3680 }
3681 self.soft_reap_buf.clear();
3682 }
3683 }
3684 _ = eviction_interval.tick() => {
3707 if self.force_immediate_choker_tick
3713 && (self.state == TorrentState::Seeding
3714 || self.state == TorrentState::Sharing)
3715 {
3716 self.slot_tuner.observe(self.upload_bytes_interval);
3717 self.choker.observe_throughput(self.upload_bytes_interval);
3718 self.upload_bytes_interval = 0;
3719 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
3720 self.run_choker().await;
3721 self.force_immediate_choker_tick = false;
3722 }
3723 if self.state != TorrentState::Seeding {
3724 let prune_cutoff = std::time::Duration::from_mins(1);
3727 while self
3728 .eviction_history
3729 .front()
3730 .copied()
3731 .is_some_and(|t| t.elapsed() > prune_cutoff)
3732 {
3733 self.eviction_history.pop_front();
3734 }
3735 let limit = self.config.proactive_evictions_per_minute_limit as usize;
3736 let window_ok = self.eviction_history.len() < limit;
3737
3738 let should_evict = window_ok
3742 && self.peer_states.as_ref().is_some_and(|ps| {
3743 let live = ps
3744 .stats
3745 .live
3746 .load(std::sync::atomic::Ordering::Relaxed);
3747 #[allow(
3748 clippy::cast_possible_truncation,
3749 clippy::cast_sign_loss
3750 )]
3751 let threshold =
3752 (self.effective_max_connections() as f32 * 0.95) as u32;
3753 debug_assert!(
3754 self.effective_max_connections()
3755 <= crate::torrent_peers::HARD_PEER_CEILING,
3756 "effective_max must be clamped to HARD_PEER_CEILING"
3757 );
3758 live >= threshold
3759 });
3760 if should_evict {
3761 let max_this_tick = 5.min(limit.saturating_sub(self.eviction_history.len()));
3764 for _ in 0..max_this_tick {
3765 match self.find_eviction_candidate() {
3766 Some((victim, pass)) => {
3767 debug!(%victim, ?pass, "v0.187.3 proactive eviction");
3768 self.disconnect_peer(victim, "proactive eviction");
3769 if matches!(pass, crate::torrent_peers::EvictionPass::ZeroThroughput)
3770 && let Some(ref ps) = self.peer_states
3771 {
3772 ps.add_eviction_ban(victim);
3773 }
3774 self.eviction_history.push_back(std::time::Instant::now());
3775 }
3776 None => break,
3777 }
3778 }
3779 }
3780
3781 self.run_piece_steal_scan();
3783 }
3784 }
3785 _ = refill_interval.tick() => {
3787 let elapsed = Duration::from_millis(100);
3788 self.upload_bucket.refill(elapsed);
3789 self.download_bucket.lock().refill(elapsed);
3790 self.rate_limiter_set.refill(elapsed);
3792 let (tcp_peers, utp_peers) = self.transport_peer_counts();
3793 self.rate_limiter_set.apply_mixed_mode(
3794 self.config.mixed_mode_algorithm,
3795 tcp_peers,
3796 utp_peers,
3797 self.config.upload_rate_limit,
3798 );
3799 }
3800 }
3801
3802 for target in std::mem::take(&mut self.holepunch_pending) {
3804 self.try_holepunch(target).await;
3805 }
3806 }
3807 }
3808
3809 pub(crate) fn distributed_copies(&self) -> (u32, u32, f32) {
3815 if self.num_pieces == 0 || self.peers.is_empty() {
3816 return (0, 0, 0.0);
3817 }
3818
3819 let num = self.num_pieces as usize;
3820 let mut availability = vec![0u32; num];
3821
3822 for peer in self.peers.values() {
3823 for idx in 0..self.num_pieces {
3824 if peer.bitfield.get(idx) {
3825 availability[idx as usize] += 1;
3826 }
3827 }
3828 }
3829
3830 let min_avail = availability.iter().copied().min().unwrap_or(0);
3831 let rarest_count = availability.iter().filter(|&&c| c == min_avail).count() as u32;
3832 let fraction = ((self.num_pieces - rarest_count) * 1000) / self.num_pieces;
3833 let copies_float = min_avail as f32 + fraction as f32 / 1000.0;
3834
3835 (min_avail, fraction, copies_float)
3836 }
3837
3838 fn build_download_queue(&self) -> Vec<PartialPieceInfo> {
3842 self.piece_owner
3843 .iter()
3844 .enumerate()
3845 .filter_map(|(piece_index, owner)| {
3846 owner.map(|_| {
3847 let piece_index = piece_index as u32;
3848 let blocks_in_piece = self
3849 .lengths
3850 .as_ref()
3851 .map_or(0, |l| l.piece_size(piece_index).div_ceil(l.chunk_size()));
3852 PartialPieceInfo {
3853 piece_index,
3854 blocks_in_piece,
3855 blocks_assigned: 0,
3856 }
3857 })
3858 })
3859 .collect()
3860 }
3861
3862 fn compute_file_progress(&self) -> Vec<u64> {
3868 let Some(meta) = self.meta.as_ref() else {
3869 return Vec::new();
3870 };
3871 let Some(lengths) = self.lengths.as_ref() else {
3872 return Vec::new();
3873 };
3874 let Some(chunk_tracker) = self.chunk_tracker.as_ref() else {
3875 return Vec::new();
3876 };
3877
3878 let files = meta.info.files();
3879 if files.is_empty() {
3880 return Vec::new();
3881 }
3882
3883 let piece_length = lengths.piece_length();
3884 let mut result = Vec::with_capacity(files.len());
3885 let mut file_offset = 0u64;
3886
3887 for file_entry in &files {
3888 let file_len = file_entry.length;
3889 if file_len == 0 {
3890 result.push(0);
3891 file_offset += file_len;
3892 continue;
3893 }
3894
3895 let file_end = file_offset + file_len;
3896 let first_piece = (file_offset / piece_length) as u32;
3897 let last_piece = ((file_end - 1) / piece_length) as u32;
3898
3899 let mut downloaded = 0u64;
3900
3901 for p in first_piece..=last_piece {
3902 if !chunk_tracker.has_piece(p) {
3903 continue;
3904 }
3905
3906 let piece_start = lengths.piece_offset(p);
3907 let piece_end = piece_start + u64::from(lengths.piece_size(p));
3908
3909 let overlap_start = piece_start.max(file_offset);
3911 let overlap_end = piece_end.min(file_end);
3912
3913 if overlap_start < overlap_end {
3914 downloaded += overlap_end - overlap_start;
3915 }
3916 }
3917
3918 result.push(downloaded);
3919 file_offset = file_end;
3920 }
3921
3922 result
3923 }
3924
3925 fn v6_retry_delay(&self) -> std::time::Duration {
3928 let base_ms: u64 = 100;
3929 let max_ms: u64 = 5000;
3930 let delay_ms = base_ms
3931 .saturating_mul(
3932 1u64.checked_shl(self.dht_v6_empty_count)
3933 .unwrap_or(u64::MAX),
3934 )
3935 .min(max_ms);
3936 std::time::Duration::from_millis(delay_ms)
3937 }
3938
3939 fn should_retry_v6(&self) -> bool {
3941 let Some(last) = self.dht_v6_last_retry else {
3942 return true; };
3944 last.elapsed() >= self.v6_retry_delay()
3945 }
3946
3947 async fn handle_force_dht_announce(&self) {
3949 if let Some(dht) = self.current_dht()
3950 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
3951 {
3952 warn!("Force DHT v4 announce failed: {e}");
3953 }
3954 if let Some(dht6) = self.current_dht_v6()
3955 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
3956 {
3957 debug!("Force DHT v6 announce failed: {e}");
3958 }
3959 if self.info_hashes.is_hybrid()
3961 && let Some(v2) = self.info_hashes.v2
3962 {
3963 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
3964 if v2_as_v1 != self.info_hash {
3965 if let Some(dht) = self.current_dht()
3966 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
3967 {
3968 debug!("Force DHT v4 dual-swarm announce failed: {e}");
3969 }
3970 if let Some(dht6) = self.current_dht_v6()
3971 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
3972 {
3973 debug!("Force DHT v6 dual-swarm announce failed: {e}");
3974 }
3975 }
3976 }
3977 }
3978
3979 async fn run_dht_requery(&mut self) {
3985 if !self.config.enable_dht {
3986 return;
3987 }
3988
3989 if self.peers.len() > self.config.max_peers.saturating_mul(4) {
3993 return;
3994 }
3995
3996 if self.dht_peers_rx.is_none()
4004 && let Some(dht) = self.current_dht()
4005 {
4006 match dht.get_peers(self.info_hash).await {
4007 Ok(rx) => self.dht_peers_rx = Some(rx),
4008 Err(e) => warn!("DHT v4 re-query failed: {e}"),
4009 }
4010 }
4011
4012 if self.dht_v6_peers_rx.is_none()
4014 && self.dht_v6_empty_count < 30
4015 && self.should_retry_v6()
4016 && let Some(dht6) = self.current_dht_v6()
4017 {
4018 self.dht_v6_last_retry = Some(std::time::Instant::now());
4019 match dht6.get_peers(self.info_hash).await {
4020 Ok(rx) => self.dht_v6_peers_rx = Some(rx),
4021 Err(e) => debug!("DHT v6 re-query failed: {e}"),
4022 }
4023 }
4024
4025 if self.info_hashes.is_hybrid()
4027 && let Some(v2) = self.info_hashes.v2
4028 {
4029 let v2_bytes: [u8; 20] = v2.0[..20]
4030 .try_into()
4031 .expect("Id32 is 32 bytes; first 20 always fit");
4032 let v2_as_v1 = Id20(v2_bytes);
4033
4034 if self.dht_v2_peers_rx.is_none()
4035 && let Some(dht) = self.current_dht()
4036 {
4037 match dht.get_peers(v2_as_v1).await {
4038 Ok(rx) => self.dht_v2_peers_rx = Some(rx),
4039 Err(e) => debug!("DHT v4 v2-swarm re-query failed: {e}"),
4040 }
4041 }
4042 if self.dht_v6_v2_peers_rx.is_none()
4043 && self.dht_v6_empty_count < 30
4044 && self.should_retry_v6()
4045 && let Some(dht6) = self.current_dht_v6()
4046 {
4047 self.dht_v6_last_retry = Some(std::time::Instant::now());
4048 match dht6.get_peers(v2_as_v1).await {
4049 Ok(rx) => self.dht_v6_v2_peers_rx = Some(rx),
4050 Err(e) => debug!("DHT v6 v2-swarm re-query failed: {e}"),
4051 }
4052 }
4053 }
4054
4055 debug!(peers = self.peers.len(), "DHT re-query triggered");
4056 }
4057
4058 async fn handle_read_piece(&self, index: u32) -> crate::Result<Bytes> {
4060 let disk = self
4061 .disk
4062 .as_ref()
4063 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4064 let lengths = self
4065 .lengths
4066 .as_ref()
4067 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4068
4069 let piece_size = lengths.piece_size(index);
4070 if piece_size == 0 {
4071 return Err(crate::Error::InvalidPieceIndex {
4072 index,
4073 num_pieces: lengths.num_pieces(),
4074 });
4075 }
4076
4077 let chunk_size = lengths.chunk_size();
4078 let num_chunks = lengths.chunks_in_piece(index);
4079 let mut buf = bytes::BytesMut::with_capacity(piece_size as usize);
4080
4081 for chunk_idx in 0..num_chunks {
4082 let begin = chunk_idx * chunk_size;
4083 let len = if chunk_idx == num_chunks - 1 {
4084 piece_size - begin
4085 } else {
4086 chunk_size
4087 };
4088 let data = disk
4089 .read_chunk(index, begin, len, DiskJobFlags::empty())
4090 .await
4091 .map_err(crate::Error::Storage)?;
4092 buf.extend_from_slice(&data);
4093 }
4094
4095 Ok(buf.freeze())
4096 }
4097
4098 async fn handle_flush_cache(&self) -> crate::Result<()> {
4100 let disk = self
4101 .disk
4102 .as_ref()
4103 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4104 disk.flush_cache().await.map_err(crate::Error::Storage)
4105 }
4106
4107 fn handle_connect_peer(&mut self, addr: SocketAddr) {
4109 if self.peers.contains_key(&addr) {
4111 return;
4112 }
4113 if let Some(ref ps) = self.peer_states {
4115 ps.add_if_not_seen(addr, PeerSource::Incoming);
4116 }
4117 }
4118
4119 pub(crate) fn fire_tracker_alerts(&self, outcomes: &[crate::tracker_manager::TrackerOutcome]) {
4121 for outcome in outcomes {
4122 match &outcome.result {
4123 Ok(num_peers) => {
4124 post_alert(
4125 &self.alert_tx,
4126 &self.alert_mask,
4127 AlertKind::TrackerReply {
4128 info_hash: self.info_hash,
4129 url: outcome.url.clone(),
4130 num_peers: *num_peers,
4131 },
4132 );
4133 }
4134 Err(msg) => {
4135 post_alert(
4136 &self.alert_tx,
4137 &self.alert_mask,
4138 AlertKind::TrackerError {
4139 info_hash: self.info_hash,
4140 url: outcome.url.clone(),
4141 message: msg.clone(),
4142 },
4143 );
4144 }
4145 }
4146 }
4147 }
4148
4149 pub(crate) fn calculate_left(&self) -> u64 {
4151 match (&self.meta, &self.chunk_tracker) {
4152 (Some(meta), Some(ct)) => {
4153 let last_piece_have = self.num_pieces > 0 && ct.bitfield().get(self.num_pieces - 1);
4154 compute_bytes_left(
4155 meta.info.total_length(),
4156 meta.info.piece_length,
4157 u64::from(self.num_pieces),
4158 u64::from(ct.bitfield().count_ones()),
4159 last_piece_have,
4160 )
4161 }
4162 _ => 0,
4163 }
4164 }
4165
4166 pub(crate) async fn shutdown_peers(&mut self) {
4167 let left = self.calculate_left();
4169 let _ = tokio::time::timeout(
4170 std::time::Duration::from_secs(3),
4171 self.tracker_manager
4172 .announce_stopped(self.uploaded, self.downloaded, left),
4173 )
4174 .await;
4175
4176 for peer in self.peers.values() {
4178 let _ = peer.cmd_tx.try_send(PeerCommand::Shutdown);
4179 }
4180 }
4181
4182 pub(crate) async fn handle_piece_data(
4185 &mut self,
4186 peer_addr: SocketAddr,
4187 index: u32,
4188 begin: u32,
4189 data: Bytes,
4190 ) {
4191 if let Some(ref ct) = self.chunk_tracker
4195 && ct.has_chunk(index, begin)
4196 {
4197 self.total_download += data.len() as u64 + 13;
4198 if let Some(peer) = self.peers.get_mut(&peer_addr) {
4202 peer.pending_requests.remove(index, begin);
4203 }
4204 if self.end_game.is_active() {
4208 self.end_game.block_received(index, begin, peer_addr);
4209 }
4210 return;
4212 }
4213
4214 let data_len = data.len();
4215
4216 if let Some(ref disk) = self.disk {
4218 disk.write_block_deferred(index, begin, data);
4219 }
4220
4221 self.downloaded += data_len as u64;
4222 self.total_download += data_len as u64 + 13; self.last_download = now_unix();
4224 self.need_save_resume = true;
4225
4226 if let Some(slab_idx) = self.peer_slab.slot_of(&peer_addr)
4228 && self.piece_owner.get(index as usize) == Some(&None)
4229 {
4230 self.piece_owner[index as usize] = Some(slab_idx);
4231 if self.inflight_started.get(index as usize) == Some(&None) {
4233 self.inflight_started[index as usize] = Some(Instant::now());
4234 }
4235 if let (Some(sc), Some(bm)) = (&self.steal_candidates, &self.block_maps)
4237 && let Some(lengths) = &self.lengths
4238 {
4239 let total_blocks = lengths.chunks_in_piece(index);
4240 if bm.next_unrequested(index, total_blocks).is_some() {
4241 sc.push(index);
4242 }
4243 }
4244 }
4245
4246 self.piece_contributors
4248 .entry(index)
4249 .or_default()
4250 .insert(peer_addr.ip());
4251
4252 let now = std::time::Instant::now();
4253 if let Some(peer) = self.peers.get_mut(&peer_addr) {
4254 peer.pending_requests.remove(index, begin);
4255 peer.download_bytes_window += data_len as u64;
4256 peer.download_bytes_total += data_len as u64;
4257 peer.pipeline.block_received(data_len as u32);
4258 peer.last_data_received = Some(now);
4259 if peer.snubbed {
4261 peer.snubbed = false;
4262 }
4263 }
4264 if self.end_game.is_active() {
4269 let cancels = self.end_game.block_received(index, begin, peer_addr);
4270 for (cancel_addr, ci, cb, cl) in cancels {
4271 if let Some(cancel_peer) = self.peers.get_mut(&cancel_addr) {
4272 let _ = cancel_peer.cmd_tx.try_send(PeerCommand::Cancel {
4273 index: ci,
4274 begin: cb,
4275 length: cl,
4276 });
4277 cancel_peer.pending_requests.remove(ci, cb);
4278 }
4279 }
4280 }
4281
4282 let piece_complete = if let Some(ref mut ct) = self.chunk_tracker {
4284 ct.chunk_received(index, begin)
4285 } else {
4286 false
4287 };
4288
4289 if piece_complete && !self.pending_verify.contains(&index) {
4290 if self.config.predictive_piece_announce_ms > 0
4292 && !self.predictive_have_sent.contains(&index)
4293 {
4294 self.predictive_have_sent.insert(index);
4295 let _ = self.have_broadcast_tx.send(index);
4296 }
4297
4298 if let Some(ref disk) = self.disk {
4301 disk.flush_piece_writes(index).await;
4302 }
4303
4304 match self.version {
4305 irontide_core::TorrentVersion::V1Only => {
4306 if let Some(ref disk) = self.disk
4308 && let Some(expected) = self
4309 .meta
4310 .as_ref()
4311 .and_then(|m| m.info.piece_hash(index as usize))
4312 {
4313 self.pending_verify.insert(index);
4314 let generation = self
4315 .piece_generations
4316 .get(index as usize)
4317 .copied()
4318 .unwrap_or(0);
4319 disk.enqueue_verify(index, expected, generation, &self.verify_result_tx);
4320 }
4321 }
4322 irontide_core::TorrentVersion::V2Only => {
4323 self.verify_and_mark_piece_v2(index).await;
4325 }
4326 irontide_core::TorrentVersion::Hybrid => {
4327 self.verify_and_mark_piece_hybrid(index).await;
4329 }
4330 }
4331 }
4332
4333 if self.end_game.is_active() {
4336 self.request_end_game_block(peer_addr).await;
4337 }
4338 }
4339
4340 pub(crate) async fn handle_piece_blocks_batch(
4345 &mut self,
4346 peer_addr: SocketAddr,
4347 blocks: Vec<crate::types::BlockEntry>,
4348 ) {
4349 for block in &blocks {
4350 self.process_block_completion(
4351 peer_addr,
4352 block.index,
4353 block.begin,
4354 block.length,
4355 block.rtt,
4356 )
4357 .await;
4358 }
4359 }
4360
4361 fn handle_open_file(
4362 &mut self,
4363 file_index: usize,
4364 ) -> crate::Result<crate::streaming::FileStreamHandle> {
4365 let meta = self
4366 .meta
4367 .as_ref()
4368 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4369 let files = meta.info.files();
4370 if file_index >= files.len() {
4371 return Err(crate::Error::InvalidFileIndex {
4372 index: file_index,
4373 count: files.len(),
4374 });
4375 }
4376 if self.file_priorities.get(file_index).copied() == Some(FilePriority::Skip) {
4377 return Err(crate::Error::FileSkipped { index: file_index });
4378 }
4379
4380 let lengths = self
4381 .lengths
4382 .as_ref()
4383 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4384 let disk = self
4385 .disk
4386 .as_ref()
4387 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
4388
4389 let mut file_offset = 0u64;
4391 for f in &files[..file_index] {
4392 file_offset += f.length;
4393 }
4394 let file_length = files[file_index].length;
4395
4396 let (cursor_tx, cursor_rx) = tokio::sync::watch::channel(0u64);
4397
4398 let permit = self
4399 .stream_read_semaphore
4400 .clone()
4401 .try_acquire_owned()
4402 .map_err(|_| crate::Error::Connection("too many concurrent stream readers".into()))?;
4403
4404 self.streaming_cursors
4406 .push(crate::streaming::StreamingCursor {
4407 file_index,
4408 file_offset,
4409 cursor_piece: (file_offset / lengths.piece_length()) as u32,
4410 readahead_pieces: self.config.readahead_pieces,
4411 cursor_rx,
4412 });
4413
4414 Ok(crate::streaming::FileStreamHandle {
4415 disk: disk.clone(),
4416 lengths: lengths.clone(),
4417 file_index,
4418 file_offset,
4419 file_length,
4420 cursor_tx,
4421 piece_ready_rx: self.piece_ready_tx.subscribe(),
4422 have: self.have_watch_rx.clone(),
4423 read_permit: permit,
4424 })
4425 }
4426
4427 async fn suggest_cached_pieces(&mut self) {
4429 if !self.config.suggest_mode {
4430 return;
4431 }
4432 let disk = match self.disk {
4433 Some(ref d) => d.clone(),
4434 None => return,
4435 };
4436 let cached = disk.cached_pieces().await;
4437 if cached.is_empty() {
4438 return;
4439 }
4440 let max_suggest = self.config.max_suggest_pieces;
4441 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4442 for peer_addr in peer_addrs {
4443 let already_suggested = self.suggested_to_peers.entry(peer_addr).or_default();
4444 let peer_has_piece = |piece: u32| -> bool {
4445 self.peers
4446 .get(&peer_addr)
4447 .is_some_and(|p| p.bitfield.get(piece))
4448 };
4449 let mut sent = 0;
4450 for &piece in &cached {
4451 if sent >= max_suggest {
4452 break;
4453 }
4454 if peer_has_piece(piece) {
4455 continue;
4456 }
4457 if already_suggested.contains(&piece) {
4458 continue;
4459 }
4460 if let Some(peer) = self.peers.get(&peer_addr) {
4461 let _ = peer.cmd_tx.try_send(PeerCommand::SuggestPiece(piece));
4462 already_suggested.insert(piece);
4463 sent += 1;
4464 }
4465 }
4466 }
4467 }
4468
4469 async fn handle_pre_resolved_metadata(&mut self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
4475 if self.state != TorrentState::FetchingMetadata {
4477 debug!(
4478 info_hash = %self.info_hash,
4479 state = ?self.state,
4480 "ignoring pre-resolved metadata: already past FetchingMetadata"
4481 );
4482 return;
4483 }
4484
4485 debug!(
4486 info_hash = %self.info_hash,
4487 info_bytes_len = info_bytes.len(),
4488 num_peers = peers.len(),
4489 "received pre-resolved metadata from background resolver"
4490 );
4491
4492 if let Some(ref mut dl) = self.metadata_downloader {
4494 dl.set_total_size(info_bytes.len() as u64);
4496
4497 let piece_size: usize = 16384;
4501 let num_pieces = info_bytes.len().div_ceil(piece_size);
4502 for i in 0..num_pieces {
4503 let start = i * piece_size;
4504 let end = (start + piece_size).min(info_bytes.len());
4505 let data = bytes::Bytes::copy_from_slice(&info_bytes[start..end]);
4506 dl.piece_received(i as u32, data);
4507 }
4508 }
4509
4510 self.try_assemble_metadata().await;
4513
4514 if !peers.is_empty() {
4516 self.handle_add_peers(peers, crate::peer_state::PeerSource::Dht);
4517 }
4518 }
4519
4520 pub(crate) async fn try_assemble_metadata(&mut self) {
4521 let assembled = if let Some(ref dl) = self.metadata_downloader {
4522 dl.assemble_and_verify()
4523 } else {
4524 return;
4525 };
4526
4527 match assembled {
4528 Ok(info_bytes) => {
4529 let mut torrent_bytes = b"d4:info".to_vec();
4535 torrent_bytes.extend_from_slice(&info_bytes);
4536 torrent_bytes.push(b'e');
4537
4538 match torrent_from_bytes(&torrent_bytes) {
4539 Ok(meta) => {
4540 let num_pieces = meta.info.num_pieces() as u32;
4541 let lengths = Lengths::new(
4542 meta.info.total_length(),
4543 meta.info.piece_length,
4544 DEFAULT_CHUNK_SIZE,
4545 );
4546
4547 let files = self.config.content_layout.apply_to_files(meta.info.files());
4552 let file_paths: Vec<std::path::PathBuf> = files
4553 .iter()
4554 .map(|f| f.path.iter().collect::<std::path::PathBuf>())
4555 .collect();
4556 let file_lengths_vec: Vec<u64> = files.iter().map(|f| f.length).collect();
4557 let prealloc_mode = self.config.preallocate_mode;
4558 let storage: Arc<dyn TorrentStorage> =
4559 match irontide_storage::FilesystemStorage::new(
4560 &self.config.download_dir,
4561 file_paths,
4562 file_lengths_vec,
4563 lengths.clone(),
4564 None,
4565 prealloc_mode,
4566 self.config.filesystem_direct_io,
4567 ) {
4568 Ok(s) => Arc::new(s),
4569 Err(e) => {
4570 warn!(
4571 "failed to create filesystem storage: {e}, falling back to memory"
4572 );
4573 Arc::new(MemoryStorage::new(lengths.clone()))
4574 }
4575 };
4576 let mut disk_handle = self
4577 .disk_manager
4578 .register_torrent(self.info_hash, storage)
4579 .await;
4580
4581 self.chunk_tracker = Some(ChunkTracker::new(lengths.clone()));
4582 self.lengths = Some(lengths);
4583 self.num_pieces = num_pieces;
4584 self.piece_generations = vec![0u64; num_pieces as usize];
4586 let (hash_tx, hash_rx) = tokio::sync::mpsc::channel(64);
4587 self.hash_result_tx = hash_tx;
4588 self.hash_result_rx = hash_rx;
4589 if let Some(ref pool) = self.hash_pool_ref {
4592 disk_handle.set_hash_pool(pool.clone());
4593 disk_handle.set_hash_result_tx(self.hash_result_tx.clone());
4594 }
4595 self.disk = Some(disk_handle);
4596 for peer in self.peers.values() {
4599 let _ = peer
4600 .cmd_tx
4601 .try_send(PeerCommand::UpdateNumPieces(num_pieces));
4602 }
4603 let file_lengths: Vec<u64> =
4604 meta.info.files().iter().map(|f| f.length).collect();
4605 let mut meta = meta;
4606 meta.info_bytes = Some(Bytes::from(info_bytes));
4607 self.meta = Some(meta);
4608
4609 if let (Some(meta), Some(lengths)) = (&self.meta, &self.lengths) {
4611 self.cached_files = Some(build_cached_file_info(meta, lengths));
4612 }
4613
4614 self.file_priorities = vec![FilePriority::Normal; file_lengths.len()];
4615
4616 if !self.config.file_priorities.is_empty() {
4620 self.file_priorities
4621 .clone_from(&self.config.file_priorities);
4622 self.file_priorities
4623 .resize(file_lengths.len(), FilePriority::Normal);
4624 self.magnet_selected_files = None;
4625 } else if let Some(ref selections) = self.magnet_selected_files {
4626 self.file_priorities = irontide_core::FileSelection::to_priorities(
4628 selections,
4629 file_lengths.len(),
4630 );
4631 self.magnet_selected_files = None;
4632 }
4633
4634 self.wanted_pieces = crate::piece_selector::build_wanted_pieces(
4635 &self.file_priorities,
4636 &file_lengths,
4637 self.lengths.as_ref().unwrap(),
4638 );
4639 if self.config.share_mode {
4640 self.transition_state(TorrentState::Sharing);
4641 } else {
4642 self.transition_state(TorrentState::Downloading);
4643 }
4644 self.metadata_downloader = None;
4645
4646 if let Some(ref meta) = self.meta {
4648 self.tracker_manager
4649 .set_metadata_filtered(meta, self.config.url_security);
4650 }
4651
4652 if let Ok(detected) = irontide_core::torrent_from_bytes_any(&torrent_bytes)
4655 {
4656 let new_version = detected.version();
4657 if new_version != irontide_core::TorrentVersion::V1Only {
4658 let new_hashes = detected.info_hashes();
4659 self.version = new_version;
4660 self.info_hashes = new_hashes.clone();
4661 self.tracker_manager.set_info_hashes(new_hashes.clone());
4662 if let Some(v2_meta) = detected.as_v2() {
4663 self.meta_v2 = Some(v2_meta.clone());
4664 }
4665 if new_hashes.is_hybrid()
4667 && let Some(v2) = new_hashes.v2
4668 {
4669 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
4670 if v2_as_v1 != self.info_hash {
4671 if self.dht_v2_peers_rx.is_none()
4672 && let Some(dht) = self.current_dht()
4673 && let Ok(rx) = dht.get_peers(v2_as_v1).await
4674 {
4675 self.dht_v2_peers_rx = Some(rx);
4676 }
4677 if self.dht_v6_v2_peers_rx.is_none()
4678 && self.dht_v6_empty_count < 30
4679 && self.should_retry_v6()
4680 && let Some(dht6) = self.current_dht_v6()
4681 && let Ok(rx) = dht6.get_peers(v2_as_v1).await
4682 {
4683 self.dht_v6_last_retry =
4684 Some(std::time::Instant::now());
4685 self.dht_v6_v2_peers_rx = Some(rx);
4686 }
4687 }
4688 }
4689 }
4690 }
4691
4692 let name = self
4693 .meta
4694 .as_ref()
4695 .map(|m| m.info.name.clone())
4696 .unwrap_or_default();
4697 post_alert(
4698 &self.alert_tx,
4699 &self.alert_mask,
4700 AlertKind::MetadataReceived {
4701 info_hash: self.info_hash,
4702 name,
4703 },
4704 );
4705 info!("metadata assembled, switching to Downloading");
4706
4707 if let Some(ct) = &self.chunk_tracker {
4709 let atomic_states = Arc::new(AtomicPieceStates::new(
4710 self.num_pieces,
4711 ct.bitfield(),
4712 &self.wanted_pieces,
4713 ));
4714 self.atomic_states = Some(Arc::clone(&atomic_states));
4715 self.piece_owner = vec![None; self.num_pieces as usize];
4716 self.inflight_started = vec![None; self.num_pieces as usize];
4718 self.max_in_flight = self.config.max_in_flight_pieces;
4719
4720 if self.config.use_block_stealing {
4722 if let Some(ref lengths) = self.lengths {
4723 self.block_maps =
4724 Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
4725 }
4726 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
4727 }
4728 self.piece_write_guards = Some(Arc::new(
4730 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
4731 ));
4732
4733 self.piece_tracker = Some(PieceTracker::new(
4735 self.num_pieces,
4736 ct.bitfield(),
4737 &self.wanted_pieces,
4738 ));
4739 if let Some(ref cached) = self.cached_files {
4740 let file_piece_ranges: Vec<(u32, u32)> = cached
4741 .entries
4742 .iter()
4743 .map(|e| (e.first_piece, e.last_piece))
4744 .collect();
4745 let om = Arc::new(PieceOrderMap::build(
4746 &self.file_priorities,
4747 &file_piece_ranges,
4748 self.num_pieces,
4749 0,
4750 self.piece_ordering(),
4751 ));
4752 self.order_map_tx.send_replace(om);
4753 }
4754
4755 let notify = Arc::new(tokio::sync::Notify::new());
4756 self.reservation_notify = Some(notify);
4757 }
4758
4759 self.spawn_web_seeds();
4761 self.assign_pieces_to_web_seeds();
4762
4763 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
4766 info!(
4767 connected_peers = peer_addrs.len(),
4768 "kick-starting piece requests for pre-connected peers"
4769 );
4770 for addr in peer_addrs {
4771 let has_bitfield =
4772 self.peers.get(&addr).map_or(0, |p| p.bitfield.count_ones());
4773 let is_choking = self.peers.get(&addr).is_none_or(|p| p.peer_choking);
4774 debug!(%addr, has_bitfield, is_choking, "post-metadata peer state");
4775 self.maybe_express_interest(addr).await;
4776 if let Some(peer) = self.peers.get(&addr)
4777 && peer.bitfield.count_ones() > 0
4778 {
4779 let _slot = self.peer_slab.insert(addr);
4780 }
4781 }
4782 self.recalc_max_in_flight();
4783 if !self.user_seed_mode
4787 && let Some(notify) = &self.reservation_notify
4788 && let Some(ref lengths) = self.lengths
4789 {
4790 for peer in self.peers.values() {
4791 let _ = peer.cmd_tx.try_send(PeerCommand::StartRequesting {
4792 piece_notify: Arc::clone(notify),
4793 disk_handle: self.disk.clone(),
4794 write_error_tx: self.write_error_tx.clone(),
4795 lengths: lengths.clone(),
4796 });
4797 }
4798 }
4799 }
4800 Err(e) => {
4801 warn!("failed to parse assembled metadata: {e}");
4802 post_alert(
4803 &self.alert_tx,
4804 &self.alert_mask,
4805 AlertKind::MetadataFailed {
4806 info_hash: self.info_hash,
4807 },
4808 );
4809 }
4810 }
4811 }
4812 Err(e) => {
4813 warn!("metadata assembly failed: {e}");
4814 post_alert(
4815 &self.alert_tx,
4816 &self.alert_mask,
4817 AlertKind::MetadataFailed {
4818 info_hash: self.info_hash,
4819 },
4820 );
4821 }
4822 }
4823 }
4824
4825 fn spawn_web_seeds(&mut self) {
4828 if !self.config.enable_web_seed {
4829 return;
4830 }
4831 let Some(meta) = &self.meta else { return };
4832 let lengths = match &self.lengths {
4833 Some(l) => l.clone(),
4834 None => return,
4835 };
4836
4837 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
4838 let file_map = irontide_storage::FileMap::new(file_lengths, lengths.clone());
4839
4840 for url in &meta.url_list {
4842 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4843 continue;
4844 }
4845 if self.web_seeds.len() >= self.config.max_web_seeds {
4846 break;
4847 }
4848
4849 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4851 warn!(%url, %e, "web seed URL rejected by security policy");
4852 continue;
4853 }
4854
4855 let url_builder = if meta.info.length.is_some() {
4856 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone())
4857 } else {
4858 let file_paths: Vec<String> = meta
4859 .info
4860 .files()
4861 .iter()
4862 .map(|f| f.path[1..].join("/")) .collect();
4864 crate::web_seed::WebSeedUrlBuilder::multi(
4865 url.clone(),
4866 meta.info.name.clone(),
4867 file_paths,
4868 )
4869 };
4870
4871 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4872 let initial_downloaded = self
4873 .web_seed_stats
4874 .get(url)
4875 .map_or(0, |s| s.downloaded_bytes);
4876 let task = crate::web_seed::WebSeedTask::new(
4877 url.clone(),
4878 crate::web_seed::WebSeedMode::GetRight,
4879 url_builder,
4880 lengths.clone(),
4881 file_map.clone(),
4882 self.info_hash,
4883 cmd_rx,
4884 self.event_tx.clone(),
4885 self.config.url_security,
4886 self.config.web_seed_progress_throttle_ms,
4887 initial_downloaded,
4888 self.config.web_seed_retry_base_secs,
4889 self.config.web_seed_retry_factor,
4890 self.config.web_seed_retry_cap_secs,
4891 self.config.web_seed_max_failures,
4892 );
4893 tokio::spawn(task.run());
4894 self.web_seeds.insert(url.clone(), cmd_tx);
4895 debug!(url, "spawned BEP 19 web seed");
4896 }
4897
4898 for url in &meta.httpseeds {
4900 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
4901 continue;
4902 }
4903 if self.web_seeds.len() >= self.config.max_web_seeds {
4904 break;
4905 }
4906
4907 if let Err(e) = crate::url_guard::validate_web_seed_url(url, self.config.url_security) {
4909 warn!(%url, %e, "web seed URL rejected by security policy");
4910 continue;
4911 }
4912
4913 let url_builder =
4915 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone());
4916
4917 let (cmd_tx, cmd_rx) = mpsc::channel(16);
4918 let initial_downloaded = self
4919 .web_seed_stats
4920 .get(url)
4921 .map_or(0, |s| s.downloaded_bytes);
4922 let task = crate::web_seed::WebSeedTask::new(
4923 url.clone(),
4924 crate::web_seed::WebSeedMode::Hoffman,
4925 url_builder,
4926 lengths.clone(),
4927 file_map.clone(),
4928 self.info_hash,
4929 cmd_rx,
4930 self.event_tx.clone(),
4931 self.config.url_security,
4932 self.config.web_seed_progress_throttle_ms,
4933 initial_downloaded,
4934 self.config.web_seed_retry_base_secs,
4935 self.config.web_seed_retry_factor,
4936 self.config.web_seed_retry_cap_secs,
4937 self.config.web_seed_max_failures,
4938 );
4939 tokio::spawn(task.run());
4940 self.web_seeds.insert(url.clone(), cmd_tx);
4941 debug!(url, "spawned BEP 17 web seed");
4942 }
4943 }
4944
4945 pub(crate) fn assign_pieces_to_web_seeds(&mut self) {
4946 if self.state != TorrentState::Downloading || self.end_game.is_active() {
4947 return;
4948 }
4949
4950 let active_urls: HashSet<&String> = self.web_seed_in_flight.values().collect();
4952 let idle_urls: Vec<String> = self
4953 .web_seeds
4954 .keys()
4955 .filter(|u| !active_urls.contains(u))
4956 .cloned()
4957 .collect();
4958
4959 let Some(ct) = &self.chunk_tracker else {
4960 return;
4961 };
4962
4963 for url in idle_urls {
4964 let piece = (0..self.num_pieces).find(|&i| {
4967 !ct.has_piece(i)
4968 && !self
4969 .piece_owner
4970 .get(i as usize)
4971 .is_some_and(std::option::Option::is_some)
4972 && !self.web_seed_in_flight.contains_key(&i)
4973 && self.wanted_pieces.get(i)
4974 });
4975
4976 if let Some(piece) = piece
4977 && let Some(cmd_tx) = self.web_seeds.get(&url)
4978 {
4979 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::FetchPiece(piece));
4980 self.web_seed_in_flight.insert(piece, url);
4981 }
4982 }
4983 }
4984
4985 pub(crate) async fn handle_web_seed_piece_data(
4986 &mut self,
4987 url: String,
4988 index: u32,
4989 data: Bytes,
4990 ) {
4991 self.web_seed_in_flight.remove(&index);
4992
4993 if let Some(ref ct) = self.chunk_tracker
4995 && ct.has_piece(index)
4996 {
4997 self.assign_pieces_to_web_seeds();
4998 return;
4999 }
5000
5001 if let Some(ref disk) = self.disk
5003 && let Err(e) = disk
5004 .write_chunk(index, 0, data.clone(), DiskJobFlags::FLUSH_PIECE)
5005 .await
5006 {
5007 warn!(index, "web seed: failed to write piece: {e}");
5008 self.assign_pieces_to_web_seeds();
5009 return;
5010 }
5011
5012 if let Some(ref mut ct) = self.chunk_tracker
5014 && let Some(ref lengths) = self.lengths
5015 {
5016 let num_chunks = lengths.chunks_in_piece(index);
5017 for chunk_idx in 0..num_chunks {
5018 if let Some((begin, _len)) = lengths.chunk_info(index, chunk_idx) {
5019 ct.chunk_received(index, begin);
5020 }
5021 }
5022 }
5023
5024 self.downloaded += data.len() as u64;
5025 self.total_download += data.len() as u64 + 13; self.last_download = now_unix();
5027 self.need_save_resume = true;
5028
5029 self.verify_and_mark_piece(index).await;
5031
5032 if let Some(ref ct) = self.chunk_tracker
5034 && !ct.has_piece(index)
5035 {
5036 self.ban_web_seed(&url);
5037 return;
5038 }
5039
5040 self.assign_pieces_to_web_seeds();
5041 }
5042
5043 pub(crate) fn handle_web_seed_error(&mut self, url: &str, piece: u32, message: &str) {
5044 self.web_seed_in_flight.remove(&piece);
5045 warn!(%url, piece, %message, "web seed error");
5046 self.assign_pieces_to_web_seeds();
5047 }
5048
5049 pub(crate) fn handle_web_seed_progress(
5057 &mut self,
5058 url: &str,
5059 bytes: u64,
5060 rate_bps: u64,
5061 error: Option<String>,
5062 ) {
5063 let now_unix = std::time::SystemTime::now()
5064 .duration_since(std::time::UNIX_EPOCH)
5065 .map_or(0, |d| d.as_secs());
5066 let entry = self
5067 .web_seed_stats
5068 .entry(url.to_owned())
5069 .or_insert_with(|| irontide_core::WebSeedStats {
5070 url: url.to_owned(),
5071 ..Default::default()
5072 });
5073 entry.downloaded_bytes = bytes;
5074 entry.last_rate_bps = rate_bps;
5075 entry.last_attempt_unix_secs = now_unix;
5076 if let Some(msg) = error {
5077 entry.state = irontide_core::WebSeedState::Errored;
5078 entry.last_error = Some(msg);
5079 entry.consecutive_failures = entry.consecutive_failures.saturating_add(1);
5080 let attempt = entry.consecutive_failures.saturating_sub(1);
5082 let secs = self
5083 .config
5084 .web_seed_retry_base_secs
5085 .saturating_mul(self.config.web_seed_retry_factor.saturating_pow(attempt))
5086 .min(self.config.web_seed_retry_cap_secs);
5087 entry.next_retry_unix_secs = Some(now_unix + secs);
5088 } else {
5089 entry.state = irontide_core::WebSeedState::Active;
5090 entry.consecutive_failures = 0;
5091 entry.next_retry_unix_secs = None;
5092 }
5093 self.need_save_resume = true;
5094 }
5095
5096 pub(crate) fn ban_web_seed(&mut self, url: &str) {
5097 warn!(%url, "banning web seed due to hash failure");
5098 self.banned_web_seeds.insert(url.to_owned());
5099
5100 if let Some(cmd_tx) = self.web_seeds.remove(url) {
5102 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::Shutdown);
5103 }
5104
5105 self.web_seed_in_flight.retain(|_, v| v != url);
5107
5108 post_alert(
5109 &self.alert_tx,
5110 &self.alert_mask,
5111 AlertKind::WebSeedBanned {
5112 info_hash: self.info_hash,
5113 url: url.to_owned(),
5114 },
5115 );
5116 }
5117
5118 async fn shutdown_web_seeds(&mut self) {
5119 for (_, cmd_tx) in self.web_seeds.drain() {
5120 let _ = cmd_tx.send(crate::web_seed::WebSeedCommand::Shutdown).await;
5121 }
5122 self.web_seed_in_flight.clear();
5123 }
5124
5125 fn refresh_peer_rates(&mut self) {
5127 self.cached_peer_rates.clear();
5128 self.cached_peer_rates.reserve(self.peers.len());
5129 for (&addr, p) in &self.peers {
5130 self.cached_peer_rates.insert(addr, p.pipeline.ewma_rate());
5131 }
5132 }
5133
5134 fn apply_request_budget(&mut self) {
5153 use std::sync::atomic::Ordering::Relaxed;
5154
5155 let churn_suspended =
5159 self.rechoke_per_min_est > crate::request_budget::CHURN_SUSPEND_FLIPS_PER_MIN;
5160 let mut choke_edges = 0u32;
5161
5162 let mut rates: Vec<crate::request_budget::PeerRate> = Vec::with_capacity(self.peers.len());
5163 for (addr, p) in &mut self.peers {
5164 if p.peer_choking && !p.prev_choking {
5165 choke_edges += 1;
5166 }
5167 p.prev_choking = p.peer_choking;
5168 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
5169 let rate = p.pipeline.ewma_rate().max(0.0) as u64;
5170 if churn_suspended {
5171 p.bdp_cap = crate::request_budget::LEGACY_DEPTH;
5176 p.bdp_shrink_streak = 0;
5177 } else if p.peer_choking {
5178 p.bdp_shrink_streak = 0;
5179 } else {
5180 let (cap, streak) = crate::request_budget::bdp_cap(
5187 p.pipeline.last_window_rate(),
5188 p.avg_rtt,
5189 p.bdp_cap,
5190 p.bdp_shrink_streak,
5191 );
5192 p.bdp_cap = cap;
5193 p.bdp_shrink_streak = streak;
5194 }
5195 rates.push((*addr, rate, !p.peer_choking, p.bdp_cap));
5196 }
5197 self.rechoke_per_min_est =
5198 0.1 * f64::from(choke_edges) * 60.0 + 0.9 * self.rechoke_per_min_est;
5199 let quotas = crate::request_budget::compute_quotas(
5200 self.config.request_budget_per_torrent,
5201 self.config.request_budget_floor,
5202 &rates,
5203 );
5204 let mut changed = false;
5205 for (addr, quota) in quotas {
5206 if let Some(peer) = self.peers.get(&addr)
5207 && peer.target_depth.swap(quota, Relaxed) != quota
5208 {
5209 changed = true;
5210 }
5211 }
5212 if changed {
5213 self.counters
5214 .inc_diag(crate::stats::BUDGET_REALLOCS_TOTAL, 1);
5215 }
5216 }
5217
5218 fn update_peer_rates(&mut self) {
5221 for peer in self.peers.values_mut() {
5222 peer.download_rate = peer.download_bytes_window / 2;
5223 peer.upload_rate = peer.upload_bytes_window / 2;
5224 peer.download_bytes_window = 0;
5225 peer.upload_bytes_window = 0;
5226 }
5227
5228 let aggregate_download: u64 = self.peers.values().map(|p| p.download_rate).sum();
5230 if aggregate_download > self.peak_download_rate {
5231 self.peak_download_rate = aggregate_download;
5232 }
5233 }
5234
5235 async fn run_choker(&mut self) {
5236 let peer_infos: Vec<ChokerPeerInfo> = self
5237 .peers
5238 .values()
5239 .map(|p| ChokerPeerInfo {
5240 addr: p.addr,
5241 download_rate: p.download_rate,
5242 upload_rate: p.upload_rate,
5243 interested: p.peer_interested,
5244 upload_only: p.upload_only,
5245 is_seed: p.upload_only
5246 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
5247 })
5248 .collect();
5249
5250 let decision = self.choker.decide(&peer_infos);
5251
5252 let mut unchoke_flips: i64 = 0;
5253 for addr in &decision.to_unchoke {
5254 if let Some(peer) = self.peers.get_mut(addr)
5255 && peer.am_choking
5256 {
5257 peer.am_choking = false;
5258 unchoke_flips += 1;
5259 if peer.am_unchoke_started_at.is_none() {
5261 peer.am_unchoke_started_at = Some(Instant::now());
5262 }
5263 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(false));
5264 }
5265 }
5266
5267 let mut choke_flips: i64 = 0;
5268 for addr in &decision.to_choke {
5269 if let Some(peer) = self.peers.get_mut(addr)
5270 && !peer.am_choking
5271 {
5272 if peer.supports_fast {
5273 let pending: Vec<(u32, u32, u32)> = peer.incoming_requests.drain(..).collect();
5274 for (index, begin, length) in pending {
5275 let _ = peer.cmd_tx.try_send(PeerCommand::RejectRequest {
5276 index,
5277 begin,
5278 length,
5279 });
5280 }
5281 }
5282 peer.am_choking = true;
5283 choke_flips += 1;
5284 if let Some(start) = peer.am_unchoke_started_at.take() {
5286 peer.unchoke_duration_total += start.elapsed();
5287 }
5288 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(true));
5289 }
5290 }
5291
5292 if unchoke_flips > 0 {
5293 self.counters
5294 .inc_diag(crate::stats::OUTBOUND_UNCHOKE_FLIPS_TOTAL, unchoke_flips);
5295 }
5296 if choke_flips > 0 {
5297 self.counters
5298 .inc_diag(crate::stats::OUTBOUND_CHOKE_FLIPS_TOTAL, choke_flips);
5299 }
5300
5301 self.serve_incoming_requests().await;
5303
5304 if self.state == TorrentState::Downloading {
5309 let zombie_threshold = Duration::from_secs(30);
5310 let zombies: Vec<SocketAddr> = self
5311 .peers
5312 .values()
5313 .filter(|p| {
5314 p.bitfield.count_ones() == 0 && p.connected_at.elapsed() > zombie_threshold
5315 })
5316 .map(|p| p.addr)
5317 .collect();
5318
5319 for &addr in &zombies {
5320 debug!(%addr, "disconnecting zombie peer (empty bitfield after 30s)");
5321 self.disconnect_peer(addr, "zombie peer (empty bitfield)");
5322 }
5323 if !zombies.is_empty() {
5324 self.recalc_max_in_flight();
5325 }
5326 }
5327 }
5328
5329 fn rotate_optimistic(&mut self) {
5330 let peer_infos: Vec<ChokerPeerInfo> = self
5331 .peers
5332 .values()
5333 .map(|p| ChokerPeerInfo {
5334 addr: p.addr,
5335 download_rate: p.download_rate,
5336 upload_rate: p.upload_rate,
5337 interested: p.peer_interested,
5338 upload_only: p.upload_only,
5339 is_seed: p.upload_only
5340 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
5341 })
5342 .collect();
5343
5344 self.choker.rotate_optimistic(&peer_infos);
5345 }
5346
5347 fn handle_i2p_incoming(&mut self, stream: crate::i2p::SamStream) {
5353 if self.peers.len() >= self.effective_max_connections() {
5354 return;
5355 }
5356
5357 let synthetic_addr = self.next_i2p_synthetic_addr();
5358
5359 let remote_dest = stream.remote_destination().clone();
5360 let dest_preview = {
5361 let b64 = remote_dest.to_base64();
5362 if b64.len() >= 8 {
5363 b64[..8].to_string()
5364 } else {
5365 b64
5366 }
5367 };
5368 self.i2p_destinations.insert(synthetic_addr, remote_dest);
5369 let tcp_stream = stream.into_inner();
5370
5371 self.spawn_peer_from_stream(synthetic_addr, tcp_stream);
5372
5373 debug!(dest = %dest_preview, addr = %synthetic_addr, "accepted I2P peer");
5374 }
5375
5376 #[allow(dead_code)] fn add_i2p_peer(
5379 &mut self,
5380 dest: crate::i2p::I2pDestination,
5381 source: PeerSource,
5382 ) -> Option<SocketAddr> {
5383 if self.i2p_destinations.values().any(|d| d == &dest) {
5385 return None;
5386 }
5387 let addr = self.next_i2p_synthetic_addr();
5388 self.i2p_destinations.insert(addr, dest);
5389 if let Some(ref ps) = self.peer_states {
5391 ps.add_if_not_seen(addr, source);
5392 }
5393 Some(addr)
5394 }
5395
5396 fn next_i2p_synthetic_addr(&mut self) -> SocketAddr {
5402 self.i2p_peer_counter = self.i2p_peer_counter.wrapping_add(1);
5403 let a = ((self.i2p_peer_counter >> 16) & 0x0F) as u8 | 0xF0;
5404 let b = ((self.i2p_peer_counter >> 8) & 0xFF) as u8;
5405 let c = (self.i2p_peer_counter & 0xFF) as u8;
5406 SocketAddr::new(
5407 std::net::IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, 1)),
5408 (self.i2p_peer_counter & 0xFFFF) as u16,
5409 )
5410 }
5411}
5412
5413pub(crate) fn is_i2p_synthetic_addr(addr: &SocketAddr) -> bool {
5415 match addr {
5416 SocketAddr::V4(v4) => v4.ip().octets()[0] & 0xF0 == 0xF0,
5417 SocketAddr::V6(_) => false,
5418 }
5419}
5420
5421async fn accept_incoming(
5424 listener: &mut Option<Box<dyn crate::transport::TransportListener>>,
5425) -> std::io::Result<(crate::transport::BoxedStream, SocketAddr)> {
5426 match listener {
5427 Some(l) => l.accept().await,
5428 None => std::future::pending().await,
5429 }
5430}
5431
5432async fn accept_i2p(
5435 rx: &mut Option<mpsc::Receiver<crate::i2p::SamStream>>,
5436) -> Option<crate::i2p::SamStream> {
5437 match rx {
5438 Some(rx) => rx.recv().await,
5439 None => std::future::pending().await,
5440 }
5441}
5442
5443pub(crate) fn serve_hashes(
5454 meta_v2: Option<&irontide_core::TorrentMetaV2>,
5455 version: irontide_core::TorrentVersion,
5456 lengths: Option<&Lengths>,
5457 request: &irontide_core::HashRequest,
5458) -> Option<Vec<irontide_core::Id32>> {
5459 let meta_v2 = match meta_v2 {
5461 Some(m) if version != irontide_core::TorrentVersion::V1Only => m,
5462 _ => return None,
5463 };
5464
5465 let piece_hashes = meta_v2.file_piece_hashes(&request.file_root)?;
5467
5468 let lengths = lengths?;
5470
5471 let blocks_per_piece = (meta_v2.info.piece_length / u64::from(lengths.chunk_size())) as u32;
5476 let num_pieces = piece_hashes.len() as u32;
5477 let num_blocks = num_pieces.saturating_mul(blocks_per_piece);
5478
5479 if !irontide_core::validate_hash_request(request, num_blocks, num_pieces) {
5480 return None;
5481 }
5482
5483 let piece_layer_base = blocks_per_piece.trailing_zeros();
5486 if request.base != piece_layer_base {
5487 return None;
5488 }
5489
5490 let start = request.index as usize;
5492 let end = (start + request.count as usize).min(piece_hashes.len());
5493 let mut hashes: Vec<irontide_core::Id32> = piece_hashes[start..end].to_vec();
5494
5495 if request.proof_layers > 0 && !piece_hashes.is_empty() {
5503 let tree = irontide_core::MerkleTree::from_leaves(&piece_hashes);
5504 let full_proof = tree.proof_path(start);
5505 let subtree_depth = if request.count > 1 {
5507 (request.count as usize)
5508 .next_power_of_two()
5509 .trailing_zeros() as usize
5510 } else {
5511 0
5512 };
5513 let available = full_proof.len().saturating_sub(subtree_depth);
5514 let proof_count = (request.proof_layers as usize).min(available);
5515 hashes.extend_from_slice(&full_proof[subtree_depth..subtree_depth + proof_count]);
5516 }
5517
5518 Some(hashes)
5519}
5520
5521#[cfg(test)]
5526impl TorrentActor {
5527 pub(crate) fn for_throttle_test(num_pieces: u32, _throttle_ms: u64) -> Self {
5542 use irontide_storage::Bitfield;
5543
5544 let config = TorrentConfig {
5545 ..TorrentConfig::default()
5546 };
5547
5548 let info_hash = Id20([0u8; 20]);
5549 let our_peer_id = Id20([0u8; 20]);
5550
5551 let (_cmd_tx, cmd_rx) = mpsc::channel(1);
5552 let (event_tx, event_rx) = mpsc::channel(1);
5553 let (write_error_tx, write_error_rx) = mpsc::channel(1);
5554 let (verify_result_tx, verify_result_rx) = mpsc::channel(1);
5555 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
5556 let (piece_ready_tx, _piece_ready_rx) = broadcast::channel(1);
5557 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(num_pieces));
5558 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
5559 let (alert_tx, _alert_rx) = broadcast::channel(64);
5560 let (_disk_mgr_tx, _disk_mgr_rx) = mpsc::channel::<crate::disk::DiskJob>(1);
5561
5562 let stream_read_semaphore = Arc::new(tokio::sync::Semaphore::new(8));
5563 let alert_mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
5564
5565 let (disk_manager, _disk_join) =
5567 crate::disk::DiskManagerHandle::new(crate::disk::DiskConfig::default());
5568
5569 let ban_manager = Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
5570 crate::ban::BanConfig::default(),
5571 )));
5572 let ip_filter = Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
5573
5574 let upload_bucket = crate::rate_limiter::TokenBucket::new(0);
5575 let download_bucket = Arc::new(parking_lot::Mutex::new(
5576 crate::rate_limiter::TokenBucket::new(0),
5577 ));
5578 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(0, 0, 0, 0, 0, 0);
5579
5580 let dht_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5581 let dht_v6_rx = irontide_dht::DhtBroadcast::new(None).subscribe();
5582 let factory = Arc::new(crate::transport::NetworkFactory::tokio());
5583
5584 let we_have = Bitfield::new(num_pieces);
5588 let mut wanted = Bitfield::new(num_pieces);
5589 for i in 0..num_pieces {
5590 wanted.set(i);
5591 }
5592 let atomic_states = Arc::new(crate::piece_reservation::AtomicPieceStates::new(
5593 num_pieces, &we_have, &wanted,
5594 ));
5595
5596 let (order_map_tx, _order_map_rx_seed) =
5597 tokio::sync::watch::channel(Arc::new(PieceOrderMap::empty()));
5598
5599 Self {
5600 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(0),
5601 config,
5602 info_hash,
5603 our_peer_id,
5604 state: TorrentState::Downloading,
5605 disk: None,
5606 disk_manager,
5607 chunk_tracker: None,
5608 lengths: None,
5609 num_pieces,
5610 file_priorities: Vec::new(),
5611 wanted_pieces: Bitfield::new(num_pieces),
5612 end_game: EndGame::new(),
5613 streaming_pieces: BTreeSet::new(),
5614 time_critical_pieces: BTreeSet::new(),
5615 streaming_cursors: Vec::new(),
5616 piece_ready_tx,
5617 have_watch_tx,
5618 have_watch_rx,
5619 stream_read_semaphore,
5620 peers: HashMap::new(),
5621 unchoke_durations: HashMap::new(),
5622 cached_peer_rates: FxHashMap::default(),
5623 refill_notify: Arc::new(tokio::sync::Notify::new()),
5624 atomic_states: Some(atomic_states),
5625 block_maps: None,
5626 steal_candidates: None,
5627 last_steal_populate: Instant::now(),
5628 piece_write_guards: None,
5629 soft_reap_buf: Vec::new(),
5630 eviction_history: std::collections::VecDeque::new(),
5631 force_immediate_choker_tick: false,
5632 piece_tracker: None,
5633 order_map_dirty: false,
5634 next_order_map_gen: 0,
5635 order_map_tx,
5636 piece_owner: vec![None; num_pieces as usize],
5637 peer_slab: crate::piece_reservation::PeerSlab::new(),
5638 priority_pieces: BTreeSet::new(),
5639 max_in_flight: 512,
5640 reservation_notify: None,
5641 last_tick_dispatch_state: None,
5642 choker: Choker::new(4),
5643 user_seed_mode: false,
5644 user_forced: false,
5645 max_connections: 0,
5646 peer_states: None,
5647 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
5648 connect_permits: HashMap::new(),
5649 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
5650 std::collections::HashMap::new(),
5651 )),
5652 connect_rx: None,
5653 metadata_downloader: None,
5654 meta: None,
5655 cached_files: None,
5656 downloaded: 0,
5657 uploaded: 0,
5658 checking_progress: 0.0,
5659 total_download: 0,
5660 total_upload: 0,
5661 total_failed_bytes: 0,
5662 total_redundant_bytes: 0,
5663 added_time: 0,
5664 completed_time: 0,
5665 last_download: 0,
5666 last_upload: 0,
5667 last_seen_complete: 0,
5668 active_duration: 0,
5669 finished_duration: 0,
5670 seeding_duration: 0,
5671 active_since: None,
5672 state_duration_since: None,
5673 started_at: Instant::now(),
5674 moving_storage: false,
5675 has_incoming: false,
5676 need_save_resume: false,
5677 error: String::new(),
5678 error_file: -1,
5679 cmd_rx,
5680 event_tx,
5681 event_rx,
5682 write_error_rx,
5683 write_error_tx,
5684 verify_result_rx,
5685 verify_result_tx,
5686 pending_verify: HashSet::new(),
5687 piece_generations: vec![0u64; num_pieces as usize],
5688 hash_result_rx,
5689 hash_result_tx,
5690 listener: None,
5691 utp_socket: None,
5692 utp_socket_v6: None,
5693 tracker_manager: TrackerManager::empty(info_hash, our_peer_id, 0, 0, false),
5694 tracker_result_rx: None,
5695 dht_rx,
5696 dht_v6_rx,
5697 dht_enabled: false,
5698 dht_peers_rx: None,
5699 dht_v6_peers_rx: None,
5700 dht_v6_empty_count: 0,
5701 dht_v6_last_retry: None,
5702 alert_tx,
5703 alert_mask,
5704 upload_bucket,
5705 download_bucket,
5706 global_upload_bucket: None,
5707 global_download_bucket: None,
5708 slot_tuner: crate::slot_tuner::SlotTuner::disabled(4),
5709 upload_bytes_interval: 0,
5710 peak_download_rate: 0,
5711 rechoke_per_min_est: 0.0,
5712 web_seeds: HashMap::new(),
5713 banned_web_seeds: HashSet::new(),
5714 web_seed_in_flight: HashMap::new(),
5715 web_seed_stats: HashMap::new(),
5716 pex_peer_count: 0,
5717 lsd_peer_count: 0,
5718 super_seed: None,
5719 have_broadcast_tx,
5720 suggested_to_peers: HashMap::new(),
5721 predictive_have_sent: HashSet::new(),
5722 ban_manager,
5723 piece_contributors: HashMap::new(),
5724 parole_pieces: HashMap::new(),
5725 ip_filter,
5726 external_ip: None,
5727 share_lru: std::collections::VecDeque::new(),
5728 share_max_pieces: 0,
5729 plugins: Arc::new(Vec::new()),
5730 hash_picker: None,
5731 version: irontide_core::TorrentVersion::V1Only,
5732 meta_v2: None,
5733 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
5734 dht_v2_peers_rx: None,
5735 dht_v6_v2_peers_rx: None,
5736 magnet_selected_files: None,
5737 sam_session: None,
5738 i2p_accept_rx: None,
5739 i2p_peer_counter: 0,
5740 i2p_destinations: HashMap::new(),
5741 ssl_manager: None,
5742 rate_limiter_set,
5743 auto_sequential_active: false,
5744 factory,
5745 hash_pool_ref: None,
5746 connect_attempts: 0,
5747 connect_failures: 0,
5748 choke_rotations: 0,
5749 inflight_started: Vec::new(),
5750 completed_piece_times: std::collections::VecDeque::new(),
5751 piece_steals: 0,
5752 holepunch_relayed: 0,
5753 holepunch_relay_rate: HashMap::new(),
5754 holepunch_cooldowns: HashMap::new(),
5755 holepunch_pending: Vec::new(),
5756 counters: Arc::new(crate::stats::SessionCounters::new()),
5757 }
5758 }
5759}
5760
5761fn compute_bytes_left(
5775 total: u64,
5776 piece_length: u64,
5777 pieces_total: u64,
5778 have: u64,
5779 last_piece_have: bool,
5780) -> u64 {
5781 if pieces_total == 0 || have >= pieces_total {
5782 return 0;
5783 }
5784 let mut downloaded = have.saturating_mul(piece_length);
5785 if last_piece_have {
5786 let last_piece_size = total.saturating_sub((pieces_total - 1).saturating_mul(piece_length));
5790 downloaded = downloaded
5791 .saturating_sub(piece_length)
5792 .saturating_add(last_piece_size);
5793 }
5794 total.saturating_sub(downloaded)
5795}
5796
5797#[cfg(test)]
5801mod tests {
5802 use super::*;
5803 use bytes::Bytes;
5804 use futures::{SinkExt, StreamExt};
5805 use irontide_wire::{ExtHandshake, Handshake, Message, MessageCodec};
5806 use std::time::Duration;
5807 use tokio::io::{AsyncReadExt, AsyncWriteExt};
5808 use tokio::net::TcpListener;
5809 use tokio_util::codec::{FramedRead, FramedWrite};
5810
5811 #[test]
5817 fn bytes_left_zero_at_completion_with_short_last_piece() {
5818 assert_eq!(compute_bytes_left(1001, 256, 4, 4, true), 0);
5820 }
5821
5822 #[test]
5823 fn bytes_left_total_at_start() {
5824 assert_eq!(compute_bytes_left(1001, 256, 4, 0, false), 1001);
5825 }
5826
5827 #[test]
5828 fn bytes_left_exact_without_last_piece() {
5829 assert_eq!(compute_bytes_left(1001, 256, 4, 2, false), 1001 - 512);
5831 }
5832
5833 #[test]
5834 fn bytes_left_exact_when_short_last_piece_verifies_early() {
5835 assert_eq!(compute_bytes_left(1001, 256, 4, 2, true), 512);
5839 }
5840
5841 #[test]
5842 fn bytes_left_exact_multiple_total_has_full_last_piece() {
5843 assert_eq!(compute_bytes_left(1024, 256, 4, 3, true), 256);
5845 assert_eq!(compute_bytes_left(1024, 256, 4, 4, true), 0);
5846 }
5847
5848 #[test]
5849 fn bytes_left_single_piece_torrent() {
5850 assert_eq!(compute_bytes_left(100, 256, 1, 0, false), 100);
5852 assert_eq!(compute_bytes_left(100, 256, 1, 1, true), 0);
5853 }
5854
5855 #[test]
5856 fn bytes_left_degenerate_inputs_saturate_without_panic() {
5857 assert_eq!(compute_bytes_left(0, 0, 0, 0, false), 0);
5859 assert_eq!(compute_bytes_left(10, 256, 4, 1, true), 10);
5863 }
5864
5865 #[test]
5868 fn initial_unchoke_slots_unlimited_returns_default_four() {
5869 assert_eq!(initial_unchoke_slots(-1), 4);
5870 }
5871
5872 #[test]
5873 fn initial_unchoke_slots_capped_returns_value() {
5874 assert_eq!(initial_unchoke_slots(1), 1);
5875 assert_eq!(initial_unchoke_slots(4), 4);
5876 assert_eq!(initial_unchoke_slots(16), 16);
5877 }
5878
5879 fn make_test_torrent(data: &[u8], piece_length: u64) -> TorrentMetaV1 {
5883 use serde::Serialize;
5884
5885 #[derive(Serialize)]
5886 struct Info<'a> {
5887 length: u64,
5888 name: &'a str,
5889 #[serde(rename = "piece length")]
5890 piece_length: u64,
5891 #[serde(with = "serde_bytes")]
5892 pieces: &'a [u8],
5893 }
5894
5895 #[derive(Serialize)]
5896 struct Torrent<'a> {
5897 info: Info<'a>,
5898 }
5899
5900 let mut pieces = Vec::new();
5901 let mut offset = 0;
5902 while offset < data.len() {
5903 let end = (offset + piece_length as usize).min(data.len());
5904 let hash = irontide_core::sha1(&data[offset..end]);
5905 pieces.extend_from_slice(hash.as_bytes());
5906 offset = end;
5907 }
5908
5909 let t = Torrent {
5910 info: Info {
5911 length: data.len() as u64,
5912 name: "test",
5913 piece_length,
5914 pieces: &pieces,
5915 },
5916 };
5917
5918 let bytes = irontide_bencode::to_bytes(&t).unwrap();
5919 torrent_from_bytes(&bytes).unwrap()
5920 }
5921
5922 fn test_config() -> TorrentConfig {
5923 TorrentConfig {
5924 listen_port: 0, max_peers: 200,
5926 target_request_queue: 5,
5927 download_dir: std::path::PathBuf::from("/tmp"),
5928 content_layout: irontide_session_types::ContentLayout::Original,
5929 enable_dht: false,
5930 enable_pex: false,
5931 enable_fast: false,
5932 seed_ratio_limit: None,
5933 seed_time_limit_secs: None,
5934 inactive_seed_time_limit_secs: None,
5935 strict_end_game: true,
5936 upload_rate_limit: 0,
5937 download_rate_limit: 0,
5938 max_uploads_per_torrent: -1,
5939 encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
5940 enable_utp: false,
5941 enable_web_seed: true,
5942 enable_holepunch: false,
5943 enable_bep40_eviction: true,
5944 max_web_seeds: 4,
5945 web_seed_retry_base_secs: 10,
5946 web_seed_retry_factor: 6,
5947 web_seed_retry_cap_secs: 3600,
5948 web_seed_max_failures: 10,
5949 super_seeding: false,
5950 upload_only_announce: true,
5951 hashing_threads: 2,
5952 sequential_download: false,
5953 prioritize_first_last_pieces: false,
5954 file_priorities: Vec::new(),
5955 initial_picker_threshold: 4,
5956 whole_pieces_threshold: 20,
5957 snub_timeout_secs: 15,
5958 readahead_pieces: 8,
5959 streaming_timeout_escalation: true,
5960 max_concurrent_stream_reads: 8,
5961 proxy: crate::proxy::ProxyConfig::default(),
5962 anonymous_mode: false,
5963 share_mode: false,
5964 enable_i2p: false,
5965 allow_i2p_mixed: false,
5966 ssl_listen_port: 0,
5967 seed_choking_algorithm: crate::choker::SeedChokingAlgorithm::FastestUpload,
5968 choking_algorithm: crate::choker::ChokingAlgorithm::FixedSlots,
5969 piece_extent_affinity: true,
5970 suggest_mode: false,
5971 max_suggest_pieces: 10,
5972 predictive_piece_announce_ms: 0,
5973 mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
5974 auto_sequential: true,
5975 preallocate_mode: irontide_storage::PreallocateMode::None,
5976 block_request_timeout_secs: 60,
5977 enable_lsd: false,
5978 force_proxy: false,
5979 steal_threshold_ratio: 10.0,
5980 steal_threshold_endgame: 3.0,
5981 peer_read_timeout_secs: 0, peer_write_timeout_secs: 0, data_contribution_timeout_secs: 0, pass0_grace_secs: 60,
5986 proactive_evictions_per_minute_limit: 30,
5987 eviction_ban_duration_secs: 600,
5988 eviction_ban_set_cap: 1024,
5989 choke_rotation_max_evictions: 0, max_concurrent_connects: 128,
5991 connect_soft_timeout: 3,
5992 dispatch_backlog_cap: 8,
5993 event_backlog_cap: 32,
5994 peer_writer_channel_cap: 1024,
5995 use_actor_dispatch: true,
5996 web_seed_progress_throttle_ms: 250,
5997 url_security: crate::url_guard::UrlSecurityConfig::default(),
5998 peer_connect_timeout: 2,
5999 peer_dscp: 0x08,
6000 initial_queue_depth: 128,
6001 max_request_queue_depth: 250,
6002 request_budget_per_torrent: 512,
6003 request_budget_floor: 8,
6004 request_queue_time: 3.0,
6005 max_metadata_size: 4 * 1024 * 1024,
6006 max_message_size: 16 * 1024 * 1024,
6007 max_piece_length: 32 * 1024 * 1024,
6008 max_outstanding_requests: 500,
6009 max_in_flight_pieces: 20,
6010 use_block_stealing: true,
6011 steal_stale_piece_secs: 2,
6012 fixed_pipeline_depth: 128,
6013 lock_warn_threshold_ms: 0, filesystem_direct_io: false,
6015 category: None,
6016 tags: Vec::new(),
6017 }
6018 }
6019
6020 fn make_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
6021 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
6022 Arc::new(MemoryStorage::new(lengths))
6023 }
6024
6025 fn make_seeded_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
6026 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
6027 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
6028 let num_pieces = lengths.num_pieces();
6030 for p in 0..num_pieces {
6031 let piece_size = lengths.piece_size(p) as usize;
6032 let offset = lengths.piece_offset(p) as usize;
6033 let end = offset + piece_size;
6034 storage.write_chunk(p, 0, &data[offset..end]).unwrap();
6035 }
6036 storage
6037 }
6038
6039 fn test_alert_channel() -> (broadcast::Sender<Alert>, Arc<AtomicU32>) {
6040 let (tx, _) = broadcast::channel(64);
6041 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
6042 (tx, mask)
6043 }
6044
6045 fn test_ban_manager() -> irontide_session_types::SharedBanManager {
6046 Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
6047 crate::ban::BanConfig::default(),
6048 )))
6049 }
6050
6051 fn test_ip_filter() -> irontide_session_types::SharedIpFilter {
6052 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()))
6053 }
6054
6055 fn test_disk_manager() -> (DiskManagerHandle, tokio::task::JoinHandle<()>) {
6056 DiskManagerHandle::new(crate::disk::DiskConfig::default())
6057 }
6058
6059 async fn test_register_disk(
6060 info_hash: Id20,
6061 storage: Arc<dyn TorrentStorage>,
6062 ) -> (DiskHandle, DiskManagerHandle, tokio::task::JoinHandle<()>) {
6063 let (dm, join) = test_disk_manager();
6064 let dh = dm.register_torrent(info_hash, storage).await;
6065 (dh, dm, join)
6066 }
6067
6068 fn test_dht_rx() -> irontide_dht::DhtReceiver {
6071 let bx = irontide_dht::DhtBroadcast::new(None);
6074 bx.subscribe()
6075 }
6076
6077 const HANDSHAKE_SIZE: usize = 68;
6079
6080 #[tokio::test]
6083 async fn create_from_torrent() {
6084 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
6087 let config = test_config();
6088
6089 let (atx, amask) = test_alert_channel();
6090 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6091 let handle = TorrentHandle::from_torrent(
6092 meta,
6093 irontide_core::TorrentVersion::V1Only,
6094 None,
6095 dh,
6096 dm,
6097 config,
6098 test_dht_rx(),
6099 test_dht_rx(),
6100 None,
6101 None,
6102 crate::slot_tuner::SlotTuner::disabled(4),
6103 atx,
6104 amask,
6105 None,
6106 None,
6107 test_ban_manager(),
6108 test_ip_filter(),
6109 Arc::new(Vec::new()),
6110 None,
6111 None,
6112 Arc::new(crate::transport::NetworkFactory::tokio()),
6113 None, Arc::new(crate::stats::SessionCounters::new()),
6115 )
6116 .await
6117 .unwrap();
6118
6119 let stats = handle.stats().await.unwrap();
6120 assert_eq!(stats.state, TorrentState::Downloading);
6121 assert_eq!(stats.pieces_total, 2);
6122 assert_eq!(stats.pieces_have, 0);
6123 assert_eq!(stats.peers_connected, 0);
6124
6125 handle.shutdown().await.unwrap();
6126 }
6127
6128 async fn started_test_handle() -> (TorrentHandle, Vec<String>, tokio::task::JoinHandle<()>) {
6137 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
6139 let expected_hex: Vec<String> =
6140 meta.info.pieces.chunks_exact(20).map(hex::encode).collect();
6141 let storage = make_storage(&data, 16384);
6142 let config = test_config();
6143
6144 let (atx, amask) = test_alert_channel();
6145 let (dh, dm, dj) = test_register_disk(meta.info_hash, storage).await;
6146 let handle = TorrentHandle::from_torrent(
6147 meta,
6148 irontide_core::TorrentVersion::V1Only,
6149 None,
6150 dh,
6151 dm,
6152 config,
6153 test_dht_rx(),
6154 test_dht_rx(),
6155 None,
6156 None,
6157 crate::slot_tuner::SlotTuner::disabled(4),
6158 atx,
6159 amask,
6160 None,
6161 None,
6162 test_ban_manager(),
6163 test_ip_filter(),
6164 Arc::new(Vec::new()),
6165 None,
6166 None,
6167 Arc::new(crate::transport::NetworkFactory::tokio()),
6168 None,
6169 Arc::new(crate::stats::SessionCounters::new()),
6170 )
6171 .await
6172 .unwrap();
6173 (handle, expected_hex, dj)
6174 }
6175
6176 #[tokio::test]
6183 async fn take_resume_if_dirty_is_atomic_capture_and_clear() {
6184 let (handle, _expected_hex, _dj) = started_test_handle().await;
6185
6186 handle.set_tags(vec!["m245".to_string()]).await.unwrap();
6190
6191 let first = handle.take_resume_if_dirty().await.unwrap();
6192 assert!(first.is_some(), "dirty torrent must yield resume data");
6193
6194 let second = handle.take_resume_if_dirty().await.unwrap();
6195 assert!(
6196 second.is_none(),
6197 "flag was cleared atomically in the same take — no second capture"
6198 );
6199
6200 handle.shutdown().await.unwrap();
6201 }
6202
6203 #[tokio::test]
6209 async fn mark_resume_dirty_restores_capture_after_write_failure() {
6210 let (handle, _expected_hex, _dj) = started_test_handle().await;
6211
6212 handle.set_tags(vec!["m245".to_string()]).await.unwrap();
6213
6214 let captured = handle.take_resume_if_dirty().await.unwrap();
6215 assert!(captured.is_some(), "dirty torrent captured once");
6216
6217 let between = handle.take_resume_if_dirty().await.unwrap();
6219 assert!(between.is_none(), "take cleared the flag");
6220
6221 handle.mark_resume_dirty().await.unwrap();
6223
6224 let recaptured = handle.take_resume_if_dirty().await.unwrap();
6225 assert!(
6226 recaptured.is_some(),
6227 "re-dirtied torrent must re-capture — no lost resume update"
6228 );
6229
6230 handle.shutdown().await.unwrap();
6231 }
6232
6233 #[tokio::test]
6240 async fn get_piece_hashes_hex_parity_and_windowing() {
6241 let (handle, expected_hex, _dj) = started_test_handle().await;
6242 assert_eq!(expected_hex.len(), 2, "2-piece test torrent");
6243
6244 let all = handle.get_piece_hashes(0, 1000).await.unwrap();
6246 assert_eq!(
6247 all, expected_hex,
6248 "hex output must match the raw piece hashes"
6249 );
6250
6251 let windowed = handle.get_piece_hashes(1, 1).await.unwrap();
6253 assert_eq!(windowed, vec![expected_hex[1].clone()]);
6254
6255 let first = handle.get_piece_hashes(0, 1).await.unwrap();
6257 assert_eq!(first, vec![expected_hex[0].clone()]);
6258
6259 let past = handle.get_piece_hashes(99, 5).await.unwrap();
6261 assert!(past.is_empty(), "offset past end yields empty");
6262
6263 let clamped = handle.get_piece_hashes(1, 1000).await.unwrap();
6265 assert_eq!(clamped, vec![expected_hex[1].clone()]);
6266
6267 handle.shutdown().await.unwrap();
6268 }
6269
6270 #[tokio::test]
6273 async fn create_from_magnet() {
6274 let magnet = Magnet {
6275 info_hashes: irontide_core::InfoHashes::v1_only(
6276 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
6277 ),
6278 display_name: Some("test".into()),
6279 trackers: vec![],
6280 peers: vec![],
6281 selected_files: None,
6282 };
6283 let config = test_config();
6284
6285 let (atx, amask) = test_alert_channel();
6286 let (dm, _dj) = test_disk_manager();
6287 let handle = TorrentHandle::from_magnet(
6288 magnet,
6289 dm,
6290 config,
6291 test_dht_rx(),
6292 test_dht_rx(),
6293 None,
6294 None,
6295 crate::slot_tuner::SlotTuner::disabled(4),
6296 atx,
6297 amask,
6298 None,
6299 None,
6300 test_ban_manager(),
6301 test_ip_filter(),
6302 Arc::new(Vec::new()),
6303 None,
6304 None,
6305 Arc::new(crate::transport::NetworkFactory::tokio()),
6306 None, Arc::new(crate::stats::SessionCounters::new()),
6308 )
6309 .await
6310 .unwrap();
6311
6312 let stats = handle.stats().await.unwrap();
6313 assert_eq!(stats.state, TorrentState::FetchingMetadata);
6314 assert_eq!(stats.pieces_total, 0);
6315
6316 handle.shutdown().await.unwrap();
6317 }
6318
6319 #[tokio::test]
6322 async fn add_peers_increases_available() {
6323 let data = vec![0xAB; 32768];
6324 let meta = make_test_torrent(&data, 16384);
6325 let storage = make_storage(&data, 16384);
6326 let config = test_config();
6327
6328 let (atx, amask) = test_alert_channel();
6329 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6330 let handle = TorrentHandle::from_torrent(
6331 meta,
6332 irontide_core::TorrentVersion::V1Only,
6333 None,
6334 dh,
6335 dm,
6336 config,
6337 test_dht_rx(),
6338 test_dht_rx(),
6339 None,
6340 None,
6341 crate::slot_tuner::SlotTuner::disabled(4),
6342 atx,
6343 amask,
6344 None,
6345 None,
6346 test_ban_manager(),
6347 test_ip_filter(),
6348 Arc::new(Vec::new()),
6349 None,
6350 None,
6351 Arc::new(crate::transport::NetworkFactory::tokio()),
6352 None, Arc::new(crate::stats::SessionCounters::new()),
6354 )
6355 .await
6356 .unwrap();
6357
6358 let listener1 = TcpListener::bind("127.0.0.1:0").await.unwrap();
6360 let addr1 = listener1.local_addr().unwrap();
6361 let listener2 = TcpListener::bind("127.0.0.1:0").await.unwrap();
6362 let addr2 = listener2.local_addr().unwrap();
6363
6364 handle
6365 .add_peers(vec![addr1, addr2], PeerSource::Tracker)
6366 .await
6367 .unwrap();
6368
6369 tokio::time::sleep(Duration::from_millis(100)).await;
6371
6372 let stats = handle.stats().await.unwrap();
6373 assert!(
6375 stats.peers_available + stats.peers_connected >= 2,
6376 "expected at least 2 peers known, got available={}, connected={}",
6377 stats.peers_available,
6378 stats.peers_connected
6379 );
6380
6381 handle.shutdown().await.unwrap();
6382 }
6383
6384 #[tokio::test]
6387 async fn stats_reporting() {
6388 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
6391 let config = test_config();
6392
6393 let (atx, amask) = test_alert_channel();
6394 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6395 let handle = TorrentHandle::from_torrent(
6396 meta,
6397 irontide_core::TorrentVersion::V1Only,
6398 None,
6399 dh,
6400 dm,
6401 config,
6402 test_dht_rx(),
6403 test_dht_rx(),
6404 None,
6405 None,
6406 crate::slot_tuner::SlotTuner::disabled(4),
6407 atx,
6408 amask,
6409 None,
6410 None,
6411 test_ban_manager(),
6412 test_ip_filter(),
6413 Arc::new(Vec::new()),
6414 None,
6415 None,
6416 Arc::new(crate::transport::NetworkFactory::tokio()),
6417 None, Arc::new(crate::stats::SessionCounters::new()),
6419 )
6420 .await
6421 .unwrap();
6422
6423 let stats = handle.stats().await.unwrap();
6424 assert_eq!(stats.state, TorrentState::Downloading);
6425 assert_eq!(stats.downloaded, 0);
6426 assert_eq!(stats.uploaded, 0);
6427 assert_eq!(stats.pieces_have, 0);
6428 assert_eq!(stats.pieces_total, 4);
6429 assert_eq!(stats.peers_connected, 0);
6430 assert_eq!(stats.peers_available, 0);
6431
6432 handle.shutdown().await.unwrap();
6433 }
6434
6435 #[tokio::test]
6438 async fn private_torrent_disables_dht_pex() {
6439 use serde::Serialize;
6441
6442 #[derive(Serialize)]
6443 struct Info<'a> {
6444 length: u64,
6445 name: &'a str,
6446 #[serde(rename = "piece length")]
6447 piece_length: u64,
6448 #[serde(with = "serde_bytes")]
6449 pieces: &'a [u8],
6450 private: i64,
6451 }
6452
6453 #[derive(Serialize)]
6454 struct Torrent<'a> {
6455 info: Info<'a>,
6456 }
6457
6458 let data = vec![0xAB; 16384];
6459 let hash = irontide_core::sha1(&data);
6460 let mut pieces = Vec::new();
6461 pieces.extend_from_slice(hash.as_bytes());
6462
6463 let t = Torrent {
6464 info: Info {
6465 length: data.len() as u64,
6466 name: "private_test",
6467 piece_length: 16384,
6468 pieces: &pieces,
6469 private: 1,
6470 },
6471 };
6472
6473 let bytes = irontide_bencode::to_bytes(&t).unwrap();
6474 let meta = torrent_from_bytes(&bytes).unwrap();
6475 assert_eq!(meta.info.private, Some(1));
6476
6477 let storage = make_storage(&data, 16384);
6478 let mut config = test_config();
6479 config.enable_dht = true;
6480 config.enable_pex = true;
6481
6482 let (atx, amask) = test_alert_channel();
6484 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6485 let handle = TorrentHandle::from_torrent(
6486 meta,
6487 irontide_core::TorrentVersion::V1Only,
6488 None,
6489 dh,
6490 dm,
6491 config,
6492 test_dht_rx(),
6493 test_dht_rx(),
6494 None,
6495 None,
6496 crate::slot_tuner::SlotTuner::disabled(4),
6497 atx,
6498 amask,
6499 None,
6500 None,
6501 test_ban_manager(),
6502 test_ip_filter(),
6503 Arc::new(Vec::new()),
6504 None,
6505 None,
6506 Arc::new(crate::transport::NetworkFactory::tokio()),
6507 None, Arc::new(crate::stats::SessionCounters::new()),
6509 )
6510 .await
6511 .unwrap();
6512
6513 let stats = handle.stats().await.unwrap();
6517 assert_eq!(stats.state, TorrentState::Downloading);
6518
6519 handle.shutdown().await.unwrap();
6520 }
6521
6522 #[tokio::test]
6525 async fn shutdown_cleanup() {
6526 let data = vec![0xAB; 16384];
6527 let meta = make_test_torrent(&data, 16384);
6528 let storage = make_storage(&data, 16384);
6529 let config = test_config();
6530
6531 let (atx, amask) = test_alert_channel();
6532 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6533 let handle = TorrentHandle::from_torrent(
6534 meta,
6535 irontide_core::TorrentVersion::V1Only,
6536 None,
6537 dh,
6538 dm,
6539 config,
6540 test_dht_rx(),
6541 test_dht_rx(),
6542 None,
6543 None,
6544 crate::slot_tuner::SlotTuner::disabled(4),
6545 atx,
6546 amask,
6547 None,
6548 None,
6549 test_ban_manager(),
6550 test_ip_filter(),
6551 Arc::new(Vec::new()),
6552 None,
6553 None,
6554 Arc::new(crate::transport::NetworkFactory::tokio()),
6555 None, Arc::new(crate::stats::SessionCounters::new()),
6557 )
6558 .await
6559 .unwrap();
6560
6561 handle.shutdown().await.unwrap();
6562
6563 tokio::time::sleep(Duration::from_millis(50)).await;
6565 let result = handle.stats().await;
6566 assert!(result.is_err());
6567 }
6568
6569 #[tokio::test]
6572 async fn duplicate_peers_ignored() {
6573 let data = vec![0xAB; 16384];
6574 let meta = make_test_torrent(&data, 16384);
6575 let storage = make_storage(&data, 16384);
6576 let config = test_config();
6577
6578 let (atx, amask) = test_alert_channel();
6579 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6580 let handle = TorrentHandle::from_torrent(
6581 meta,
6582 irontide_core::TorrentVersion::V1Only,
6583 None,
6584 dh,
6585 dm,
6586 config,
6587 test_dht_rx(),
6588 test_dht_rx(),
6589 None,
6590 None,
6591 crate::slot_tuner::SlotTuner::disabled(4),
6592 atx,
6593 amask,
6594 None,
6595 None,
6596 test_ban_manager(),
6597 test_ip_filter(),
6598 Arc::new(Vec::new()),
6599 None,
6600 None,
6601 Arc::new(crate::transport::NetworkFactory::tokio()),
6602 None, Arc::new(crate::stats::SessionCounters::new()),
6604 )
6605 .await
6606 .unwrap();
6607
6608 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6610 let addr = listener.local_addr().unwrap();
6611 handle
6612 .add_peers(vec![addr, addr, addr], PeerSource::Tracker)
6613 .await
6614 .unwrap();
6615
6616 tokio::time::sleep(Duration::from_millis(100)).await;
6617 let stats = handle.stats().await.unwrap();
6618 assert!(
6620 stats.peers_available + stats.peers_connected <= 1,
6621 "expected at most 1 unique peer, got available={}, connected={}",
6622 stats.peers_available,
6623 stats.peers_connected
6624 );
6625
6626 handle.shutdown().await.unwrap();
6627 }
6628
6629 #[tokio::test]
6632 async fn cloned_handle_shares_actor() {
6633 let data = vec![0xAB; 16384];
6634 let meta = make_test_torrent(&data, 16384);
6635 let storage = make_storage(&data, 16384);
6636 let config = test_config();
6637
6638 let (atx, amask) = test_alert_channel();
6639 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6640 let handle = TorrentHandle::from_torrent(
6641 meta,
6642 irontide_core::TorrentVersion::V1Only,
6643 None,
6644 dh,
6645 dm,
6646 config,
6647 test_dht_rx(),
6648 test_dht_rx(),
6649 None,
6650 None,
6651 crate::slot_tuner::SlotTuner::disabled(4),
6652 atx,
6653 amask,
6654 None,
6655 None,
6656 test_ban_manager(),
6657 test_ip_filter(),
6658 Arc::new(Vec::new()),
6659 None,
6660 None,
6661 Arc::new(crate::transport::NetworkFactory::tokio()),
6662 None, Arc::new(crate::stats::SessionCounters::new()),
6664 )
6665 .await
6666 .unwrap();
6667 let handle2 = handle.clone();
6668
6669 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6671 let peer_addr = listener.local_addr().unwrap();
6672
6673 handle
6675 .add_peers(vec![peer_addr], PeerSource::Tracker)
6676 .await
6677 .unwrap();
6678
6679 tokio::time::sleep(Duration::from_millis(100)).await;
6680
6681 let stats = handle2.stats().await.unwrap();
6683 assert!(
6684 stats.peers_available + stats.peers_connected >= 1,
6685 "expected at least 1 peer known, got available={}, connected={}",
6686 stats.peers_available,
6687 stats.peers_connected
6688 );
6689
6690 handle.shutdown().await.unwrap();
6691 }
6692
6693 #[tokio::test]
6696 async fn peer_connect_and_disconnect_via_listener() {
6697 let data = vec![0xAB; 16384];
6698 let meta = make_test_torrent(&data, 16384);
6699 let info_hash = meta.info_hash;
6700 let storage = make_storage(&data, 16384);
6701
6702 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6704 let listen_addr = listener.local_addr().unwrap();
6705
6706 let config = TorrentConfig {
6707 listen_port: listen_addr.port(),
6708 ..test_config()
6709 };
6710
6711 drop(listener);
6713
6714 let (atx, amask) = test_alert_channel();
6715 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6716 let handle = TorrentHandle::from_torrent(
6717 meta,
6718 irontide_core::TorrentVersion::V1Only,
6719 None,
6720 dh,
6721 dm,
6722 config,
6723 test_dht_rx(),
6724 test_dht_rx(),
6725 None,
6726 None,
6727 crate::slot_tuner::SlotTuner::disabled(4),
6728 atx,
6729 amask,
6730 None,
6731 None,
6732 test_ban_manager(),
6733 test_ip_filter(),
6734 Arc::new(Vec::new()),
6735 None,
6736 None,
6737 Arc::new(crate::transport::NetworkFactory::tokio()),
6738 None, Arc::new(crate::stats::SessionCounters::new()),
6740 )
6741 .await
6742 .unwrap();
6743
6744 tokio::time::sleep(Duration::from_millis(50)).await;
6746
6747 let mut stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6749
6750 let remote_id = Id20::from_hex("1111111111111111111111111111111111111111").unwrap();
6752 let remote_hs = Handshake::new(info_hash, remote_id);
6753 stream.write_all(&remote_hs.to_bytes()).await.unwrap();
6754 stream.flush().await.unwrap();
6755
6756 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6757 stream.read_exact(&mut hs_buf).await.unwrap();
6758 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
6759 assert_eq!(their_hs.info_hash, info_hash);
6760
6761 tokio::time::sleep(Duration::from_millis(100)).await;
6763
6764 let stats = handle.stats().await.unwrap();
6765 assert_eq!(stats.peers_connected, 1);
6766
6767 drop(stream);
6769
6770 tokio::time::sleep(Duration::from_millis(200)).await;
6772
6773 let stats = handle.stats().await.unwrap();
6774 assert_eq!(stats.peers_connected, 0);
6775
6776 handle.shutdown().await.unwrap();
6777 }
6778
6779 #[tokio::test]
6785 async fn piece_download_and_verify() {
6786 let data = vec![0xCDu8; 16384];
6788 let meta = make_test_torrent(&data, 16384);
6789 let info_hash = meta.info_hash;
6790 let storage = make_storage(&data, 16384);
6791
6792 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6793 let listen_addr = listener.local_addr().unwrap();
6794 drop(listener);
6795
6796 let config = TorrentConfig {
6797 listen_port: listen_addr.port(),
6798 ..test_config()
6799 };
6800
6801 let (atx, amask) = test_alert_channel();
6802 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6803 let handle = TorrentHandle::from_torrent(
6804 meta,
6805 irontide_core::TorrentVersion::V1Only,
6806 None,
6807 dh,
6808 dm,
6809 config,
6810 test_dht_rx(),
6811 test_dht_rx(),
6812 None,
6813 None,
6814 crate::slot_tuner::SlotTuner::disabled(4),
6815 atx,
6816 amask,
6817 None,
6818 None,
6819 test_ban_manager(),
6820 test_ip_filter(),
6821 Arc::new(Vec::new()),
6822 None,
6823 None,
6824 Arc::new(crate::transport::NetworkFactory::tokio()),
6825 None, Arc::new(crate::stats::SessionCounters::new()),
6827 )
6828 .await
6829 .unwrap();
6830
6831 tokio::time::sleep(Duration::from_millis(50)).await;
6832
6833 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6835 let remote_id = Id20::from_hex("2222222222222222222222222222222222222222").unwrap();
6836
6837 let mock_data = data.clone();
6839 let mock_task = tokio::spawn(async move {
6840 let (reader, writer) = tokio::io::split(stream);
6841 let mut reader = reader;
6842 let mut writer = writer;
6843
6844 let hs = Handshake::new(info_hash, remote_id);
6846 writer.write_all(&hs.to_bytes()).await.unwrap();
6847 writer.flush().await.unwrap();
6848
6849 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6850 reader.read_exact(&mut hs_buf).await.unwrap();
6851
6852 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6854 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6855
6856 let _msg = framed_read.next().await;
6858
6859 let ext_hs = ExtHandshake::new();
6861 let payload = ext_hs.to_bytes().unwrap();
6862 framed_write
6863 .send(Message::Extended { ext_id: 0, payload })
6864 .await
6865 .unwrap();
6866
6867 let mut bf = Bitfield::new(1);
6869 bf.set(0);
6870 framed_write
6871 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6872 .await
6873 .unwrap();
6874
6875 framed_write.send(Message::Unchoke).await.unwrap();
6877
6878 while let Some(Ok(msg)) = framed_read.next().await {
6880 if let Message::Request {
6881 index,
6882 begin,
6883 length,
6884 } = msg
6885 {
6886 let start = begin as usize;
6887 let end = start + length as usize;
6888 let piece_data = &mock_data[start..end];
6889 framed_write
6890 .send(Message::Piece {
6891 index,
6892 begin,
6893 data_0: Bytes::copy_from_slice(piece_data),
6894 data_1: Bytes::new(),
6895 })
6896 .await
6897 .unwrap();
6898 }
6899 }
6900 });
6901
6902 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6904 loop {
6905 tokio::time::sleep(Duration::from_millis(100)).await;
6906 let stats = handle.stats().await.unwrap();
6907 if stats.state == TorrentState::Seeding {
6908 assert_eq!(stats.pieces_have, 1);
6909 assert_eq!(stats.pieces_total, 1);
6910 break;
6911 }
6912 if tokio::time::Instant::now() > deadline {
6913 let stats = handle.stats().await.unwrap();
6914 panic!(
6915 "download did not complete within 5s, state={:?}, have={}/{}",
6916 stats.state, stats.pieces_have, stats.pieces_total
6917 );
6918 }
6919 }
6920
6921 handle.shutdown().await.unwrap();
6922 mock_task.abort();
6923 }
6924
6925 #[tokio::test]
6928 async fn failed_piece_verification() {
6929 let data = vec![0xEEu8; 16384];
6931 let meta = make_test_torrent(&data, 16384);
6932 let info_hash = meta.info_hash;
6933 let storage = make_storage(&data, 16384);
6934
6935 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6936 let listen_addr = listener.local_addr().unwrap();
6937 drop(listener);
6938
6939 let config = TorrentConfig {
6940 listen_port: listen_addr.port(),
6941 ..test_config()
6942 };
6943
6944 let (atx, amask) = test_alert_channel();
6945 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6946 let handle = TorrentHandle::from_torrent(
6947 meta,
6948 irontide_core::TorrentVersion::V1Only,
6949 None,
6950 dh,
6951 dm,
6952 config,
6953 test_dht_rx(),
6954 test_dht_rx(),
6955 None,
6956 None,
6957 crate::slot_tuner::SlotTuner::disabled(4),
6958 atx,
6959 amask,
6960 None,
6961 None,
6962 test_ban_manager(),
6963 test_ip_filter(),
6964 Arc::new(Vec::new()),
6965 None,
6966 None,
6967 Arc::new(crate::transport::NetworkFactory::tokio()),
6968 None, Arc::new(crate::stats::SessionCounters::new()),
6970 )
6971 .await
6972 .unwrap();
6973
6974 tokio::time::sleep(Duration::from_millis(50)).await;
6975
6976 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6978 let remote_id = Id20::from_hex("3333333333333333333333333333333333333333").unwrap();
6979
6980 let correct_data = data.clone();
6981 let mock_task = tokio::spawn(async move {
6982 let (reader, writer) = tokio::io::split(stream);
6983
6984 let mut writer = writer;
6986 let mut reader = reader;
6987 let hs = Handshake::new(info_hash, remote_id);
6988 writer.write_all(&hs.to_bytes()).await.unwrap();
6989 writer.flush().await.unwrap();
6990
6991 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6992 reader.read_exact(&mut hs_buf).await.unwrap();
6993
6994 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6995 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6996
6997 let _msg = framed_read.next().await;
6999
7000 let ext_hs = ExtHandshake::new();
7002 let payload = ext_hs.to_bytes().unwrap();
7003 framed_write
7004 .send(Message::Extended { ext_id: 0, payload })
7005 .await
7006 .unwrap();
7007
7008 let mut bf = Bitfield::new(1);
7010 bf.set(0);
7011 framed_write
7012 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7013 .await
7014 .unwrap();
7015
7016 framed_write.send(Message::Unchoke).await.unwrap();
7018
7019 let mut request_count = 0u32;
7020 while let Some(Ok(msg)) = framed_read.next().await {
7021 if let Message::Request {
7022 index,
7023 begin,
7024 length,
7025 } = msg
7026 {
7027 request_count += 1;
7028 let piece_data = if request_count <= 1 {
7029 vec![0xFF; length as usize]
7031 } else {
7032 let start = begin as usize;
7034 let end = start + length as usize;
7035 correct_data[start..end].to_vec()
7036 };
7037 framed_write
7038 .send(Message::Piece {
7039 index,
7040 begin,
7041 data_0: Bytes::from(piece_data),
7042 data_1: Bytes::new(),
7043 })
7044 .await
7045 .unwrap();
7046 }
7047 }
7048 });
7049
7050 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7052 loop {
7053 tokio::time::sleep(Duration::from_millis(100)).await;
7054 let stats = handle.stats().await.unwrap();
7055 if stats.state == TorrentState::Seeding {
7056 assert_eq!(stats.pieces_have, 1);
7057 break;
7058 }
7059 if tokio::time::Instant::now() > deadline {
7060 let stats = handle.stats().await.unwrap();
7061 panic!(
7062 "download did not complete after retry within 5s, state={:?}, have={}",
7063 stats.state, stats.pieces_have,
7064 );
7065 }
7066 }
7067
7068 handle.shutdown().await.unwrap();
7069 mock_task.abort();
7070 }
7071
7072 #[tokio::test]
7075 async fn complete_transitions_state() {
7076 let data = vec![0xBBu8; 32768];
7078 let meta = make_test_torrent(&data, 16384);
7079 let info_hash = meta.info_hash;
7080 let storage = make_storage(&data, 16384);
7081
7082 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7083 let listen_addr = listener.local_addr().unwrap();
7084 drop(listener);
7085
7086 let config = TorrentConfig {
7087 listen_port: listen_addr.port(),
7088 ..test_config()
7089 };
7090
7091 let (atx, amask) = test_alert_channel();
7092 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7093 let handle = TorrentHandle::from_torrent(
7094 meta,
7095 irontide_core::TorrentVersion::V1Only,
7096 None,
7097 dh,
7098 dm,
7099 config,
7100 test_dht_rx(),
7101 test_dht_rx(),
7102 None,
7103 None,
7104 crate::slot_tuner::SlotTuner::disabled(4),
7105 atx,
7106 amask,
7107 None,
7108 None,
7109 test_ban_manager(),
7110 test_ip_filter(),
7111 Arc::new(Vec::new()),
7112 None,
7113 None,
7114 Arc::new(crate::transport::NetworkFactory::tokio()),
7115 None, Arc::new(crate::stats::SessionCounters::new()),
7117 )
7118 .await
7119 .unwrap();
7120
7121 tokio::time::sleep(Duration::from_millis(50)).await;
7122
7123 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7125 let remote_id = Id20::from_hex("4444444444444444444444444444444444444444").unwrap();
7126
7127 let mock_data = data.clone();
7128 let mock_task = tokio::spawn(async move {
7129 let (reader, writer) = tokio::io::split(stream);
7130 let mut writer = writer;
7131 let mut reader = reader;
7132
7133 let hs = Handshake::new(info_hash, remote_id);
7134 writer.write_all(&hs.to_bytes()).await.unwrap();
7135 writer.flush().await.unwrap();
7136
7137 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7138 reader.read_exact(&mut hs_buf).await.unwrap();
7139
7140 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7141 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7142
7143 let _msg = framed_read.next().await;
7145
7146 let ext_hs = ExtHandshake::new();
7148 let payload = ext_hs.to_bytes().unwrap();
7149 framed_write
7150 .send(Message::Extended { ext_id: 0, payload })
7151 .await
7152 .unwrap();
7153
7154 let mut bf = Bitfield::new(2);
7156 bf.set(0);
7157 bf.set(1);
7158 framed_write
7159 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7160 .await
7161 .unwrap();
7162
7163 framed_write.send(Message::Unchoke).await.unwrap();
7164
7165 while let Some(Ok(msg)) = framed_read.next().await {
7166 if let Message::Request {
7167 index,
7168 begin,
7169 length,
7170 } = msg
7171 {
7172 let abs_start = (index as usize * 16384) + begin as usize;
7173 let abs_end = abs_start + length as usize;
7174 let piece_data = &mock_data[abs_start..abs_end];
7175 framed_write
7176 .send(Message::Piece {
7177 index,
7178 begin,
7179 data_0: Bytes::copy_from_slice(piece_data),
7180 data_1: Bytes::new(),
7181 })
7182 .await
7183 .unwrap();
7184 }
7185 }
7186 });
7187
7188 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7189 loop {
7190 tokio::time::sleep(Duration::from_millis(100)).await;
7191 let stats = handle.stats().await.unwrap();
7192 if stats.state == TorrentState::Seeding {
7193 assert_eq!(stats.pieces_have, 2);
7194 assert_eq!(stats.pieces_total, 2);
7195 break;
7196 }
7197 if tokio::time::Instant::now() > deadline {
7198 let stats = handle.stats().await.unwrap();
7199 panic!(
7200 "expected Complete, got {:?}, have={}/{}",
7201 stats.state, stats.pieces_have, stats.pieces_total
7202 );
7203 }
7204 }
7205
7206 handle.shutdown().await.unwrap();
7207 mock_task.abort();
7208 }
7209
7210 #[tokio::test]
7213 async fn multi_chunk_piece_download() {
7214 let data = vec![0xAAu8; 32768];
7216 let meta = make_test_torrent(&data, 32768);
7217 let info_hash = meta.info_hash;
7218 let storage = make_storage(&data, 32768);
7219
7220 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7221 let listen_addr = listener.local_addr().unwrap();
7222 drop(listener);
7223
7224 let config = TorrentConfig {
7225 listen_port: listen_addr.port(),
7226 ..test_config()
7227 };
7228
7229 let (atx, amask) = test_alert_channel();
7230 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7231 let handle = TorrentHandle::from_torrent(
7232 meta,
7233 irontide_core::TorrentVersion::V1Only,
7234 None,
7235 dh,
7236 dm,
7237 config,
7238 test_dht_rx(),
7239 test_dht_rx(),
7240 None,
7241 None,
7242 crate::slot_tuner::SlotTuner::disabled(4),
7243 atx,
7244 amask,
7245 None,
7246 None,
7247 test_ban_manager(),
7248 test_ip_filter(),
7249 Arc::new(Vec::new()),
7250 None,
7251 None,
7252 Arc::new(crate::transport::NetworkFactory::tokio()),
7253 None, Arc::new(crate::stats::SessionCounters::new()),
7255 )
7256 .await
7257 .unwrap();
7258
7259 tokio::time::sleep(Duration::from_millis(50)).await;
7260
7261 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7262 let remote_id = Id20::from_hex("5555555555555555555555555555555555555555").unwrap();
7263
7264 let mock_data = data.clone();
7265 let mock_task = tokio::spawn(async move {
7266 let (reader, writer) = tokio::io::split(stream);
7267 let mut writer = writer;
7268 let mut reader = reader;
7269
7270 let hs = Handshake::new(info_hash, remote_id);
7271 writer.write_all(&hs.to_bytes()).await.unwrap();
7272 writer.flush().await.unwrap();
7273
7274 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7275 reader.read_exact(&mut hs_buf).await.unwrap();
7276
7277 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7278 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7279
7280 let _msg = framed_read.next().await;
7281
7282 let ext_hs = ExtHandshake::new();
7283 let payload = ext_hs.to_bytes().unwrap();
7284 framed_write
7285 .send(Message::Extended { ext_id: 0, payload })
7286 .await
7287 .unwrap();
7288
7289 let mut bf = Bitfield::new(1);
7290 bf.set(0);
7291 framed_write
7292 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7293 .await
7294 .unwrap();
7295
7296 framed_write.send(Message::Unchoke).await.unwrap();
7297
7298 while let Some(Ok(msg)) = framed_read.next().await {
7299 if let Message::Request {
7300 index: _,
7301 begin,
7302 length,
7303 } = msg
7304 {
7305 let start = begin as usize;
7306 let end = start + length as usize;
7307 framed_write
7308 .send(Message::Piece {
7309 index: 0,
7310 begin,
7311 data_0: Bytes::copy_from_slice(&mock_data[start..end]),
7312 data_1: Bytes::new(),
7313 })
7314 .await
7315 .unwrap();
7316 }
7317 }
7318 });
7319
7320 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7321 loop {
7322 tokio::time::sleep(Duration::from_millis(100)).await;
7323 let stats = handle.stats().await.unwrap();
7324 if stats.state == TorrentState::Seeding {
7325 assert_eq!(stats.pieces_have, 1);
7326 break;
7327 }
7328 assert!(
7329 tokio::time::Instant::now() <= deadline,
7330 "multi-chunk download did not complete within 5s"
7331 );
7332 }
7333
7334 handle.shutdown().await.unwrap();
7335 mock_task.abort();
7336 }
7337
7338 #[tokio::test]
7341 async fn seeder_leecher_integration() {
7342 let data = vec![0xDDu8; 32768]; let piece_length = 16384u64;
7345 let meta = make_test_torrent(&data, piece_length);
7346 let info_hash = meta.info_hash;
7347
7348 let seeder_storage = make_seeded_storage(&data, piece_length);
7350
7351 let seeder_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7357 let seeder_addr = seeder_listener.local_addr().unwrap();
7358
7359 let seeder_task = tokio::spawn(async move {
7360 let (stream, _addr) = seeder_listener.accept().await.unwrap();
7361 let (reader, writer) = tokio::io::split(stream);
7362 let mut writer = writer;
7363 let mut reader = reader;
7364
7365 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7367 reader.read_exact(&mut hs_buf).await.unwrap();
7368 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
7369 assert_eq!(their_hs.info_hash, info_hash);
7370
7371 let hs = Handshake::new(info_hash, PeerId::generate().0);
7372 writer.write_all(&hs.to_bytes()).await.unwrap();
7373 writer.flush().await.unwrap();
7374
7375 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7376 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7377
7378 let _msg = framed_read.next().await;
7380
7381 let ext_hs = ExtHandshake::new();
7383 let payload = ext_hs.to_bytes().unwrap();
7384 framed_write
7385 .send(Message::Extended { ext_id: 0, payload })
7386 .await
7387 .unwrap();
7388
7389 let mut bf = Bitfield::new(2);
7391 bf.set(0);
7392 bf.set(1);
7393 framed_write
7394 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7395 .await
7396 .unwrap();
7397
7398 framed_write.send(Message::Unchoke).await.unwrap();
7400
7401 while let Some(Ok(msg)) = framed_read.next().await {
7403 if let Message::Request {
7404 index,
7405 begin,
7406 length,
7407 } = msg
7408 {
7409 let piece_data = seeder_storage.read_chunk(index, begin, length).unwrap();
7410 framed_write
7411 .send(Message::Piece {
7412 index,
7413 begin,
7414 data_0: Bytes::from(piece_data),
7415 data_1: Bytes::new(),
7416 })
7417 .await
7418 .unwrap();
7419 }
7420 }
7421 });
7422
7423 let leecher_storage = make_storage(&data, piece_length);
7425 let leecher_meta = make_test_torrent(&data, piece_length);
7426
7427 let leecher_config = test_config();
7428 let (latx, lamask) = test_alert_channel();
7429 let (ldh, ldm, _ldj) = test_register_disk(leecher_meta.info_hash, leecher_storage).await;
7430 let leecher = TorrentHandle::from_torrent(
7431 leecher_meta,
7432 irontide_core::TorrentVersion::V1Only,
7433 None,
7434 ldh,
7435 ldm,
7436 leecher_config,
7437 test_dht_rx(),
7438 test_dht_rx(),
7439 None,
7440 None,
7441 crate::slot_tuner::SlotTuner::disabled(4),
7442 latx,
7443 lamask,
7444 None,
7445 None,
7446 test_ban_manager(),
7447 test_ip_filter(),
7448 Arc::new(Vec::new()),
7449 None,
7450 None,
7451 Arc::new(crate::transport::NetworkFactory::tokio()),
7452 None, Arc::new(crate::stats::SessionCounters::new()),
7454 )
7455 .await
7456 .unwrap();
7457
7458 leecher
7460 .add_peers(vec![seeder_addr], PeerSource::Tracker)
7461 .await
7462 .unwrap();
7463
7464 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
7468 loop {
7469 tokio::time::sleep(Duration::from_millis(200)).await;
7470 let stats = leecher.stats().await.unwrap();
7471 if stats.state == TorrentState::Seeding {
7472 assert_eq!(stats.pieces_have, 2);
7473 assert_eq!(stats.pieces_total, 2);
7474 break;
7475 }
7476 if tokio::time::Instant::now() > deadline {
7477 let stats = leecher.stats().await.unwrap();
7478 panic!(
7479 "seeder/leecher: leecher did not complete, state={:?}, have={}/{}, connected={}, available={}",
7480 stats.state,
7481 stats.pieces_have,
7482 stats.pieces_total,
7483 stats.peers_connected,
7484 stats.peers_available,
7485 );
7486 }
7487 }
7488
7489 leecher.shutdown().await.unwrap();
7490 seeder_task.abort();
7491 }
7492
7493 #[tokio::test]
7496 async fn magnet_initial_stats() {
7497 let magnet = Magnet {
7498 info_hashes: irontide_core::InfoHashes::v1_only(
7499 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
7500 ),
7501 display_name: Some("magnet test".into()),
7502 trackers: vec![],
7503 peers: vec![],
7504 selected_files: None,
7505 };
7506
7507 let (atx, amask) = test_alert_channel();
7508 let (dm, _dj) = test_disk_manager();
7509 let handle = TorrentHandle::from_magnet(
7510 magnet,
7511 dm,
7512 test_config(),
7513 test_dht_rx(),
7514 test_dht_rx(),
7515 None,
7516 None,
7517 crate::slot_tuner::SlotTuner::disabled(4),
7518 atx,
7519 amask,
7520 None,
7521 None,
7522 test_ban_manager(),
7523 test_ip_filter(),
7524 Arc::new(Vec::new()),
7525 None,
7526 None,
7527 Arc::new(crate::transport::NetworkFactory::tokio()),
7528 None, Arc::new(crate::stats::SessionCounters::new()),
7530 )
7531 .await
7532 .unwrap();
7533
7534 let stats = handle.stats().await.unwrap();
7535 assert_eq!(stats.state, TorrentState::FetchingMetadata);
7536 assert_eq!(stats.pieces_total, 0);
7537 assert_eq!(stats.pieces_have, 0);
7538 assert_eq!(stats.downloaded, 0);
7539 assert_eq!(stats.uploaded, 0);
7540 assert_eq!(stats.peers_connected, 0);
7541 assert_eq!(stats.peers_available, 0);
7542
7543 handle.shutdown().await.unwrap();
7544 }
7545
7546 #[tokio::test]
7549 async fn tracker_populated_from_metadata() {
7550 use serde::Serialize;
7551
7552 #[derive(Serialize)]
7553 struct Info<'a> {
7554 length: u64,
7555 name: &'a str,
7556 #[serde(rename = "piece length")]
7557 piece_length: u64,
7558 #[serde(with = "serde_bytes")]
7559 pieces: &'a [u8],
7560 }
7561
7562 #[derive(Serialize)]
7563 struct Torrent<'a> {
7564 announce: &'a str,
7565 info: Info<'a>,
7566 }
7567
7568 let data = vec![0xAB; 16384];
7569 let hash = irontide_core::sha1(&data);
7570 let mut pieces = Vec::new();
7571 pieces.extend_from_slice(hash.as_bytes());
7572
7573 let t = Torrent {
7574 announce: "http://tracker.example.com:8080/announce",
7575 info: Info {
7576 length: data.len() as u64,
7577 name: "test",
7578 piece_length: 16384,
7579 pieces: &pieces,
7580 },
7581 };
7582
7583 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7584 let meta = torrent_from_bytes(&bytes).unwrap();
7585 assert!(meta.announce.is_some());
7586
7587 let storage = make_storage(&data, 16384);
7588 let config = test_config();
7589
7590 let (atx, amask) = test_alert_channel();
7593 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7594 let handle = TorrentHandle::from_torrent(
7595 meta,
7596 irontide_core::TorrentVersion::V1Only,
7597 None,
7598 dh,
7599 dm,
7600 config,
7601 test_dht_rx(),
7602 test_dht_rx(),
7603 None,
7604 None,
7605 crate::slot_tuner::SlotTuner::disabled(4),
7606 atx,
7607 amask,
7608 None,
7609 None,
7610 test_ban_manager(),
7611 test_ip_filter(),
7612 Arc::new(Vec::new()),
7613 None,
7614 None,
7615 Arc::new(crate::transport::NetworkFactory::tokio()),
7616 None, Arc::new(crate::stats::SessionCounters::new()),
7618 )
7619 .await
7620 .unwrap();
7621
7622 let stats = handle.stats().await.unwrap();
7623 assert_eq!(stats.state, TorrentState::Downloading);
7624
7625 handle.shutdown().await.unwrap();
7626 }
7627
7628 #[tokio::test]
7631 async fn private_torrent_no_dht_field() {
7632 use serde::Serialize;
7633
7634 #[derive(Serialize)]
7635 struct Info<'a> {
7636 length: u64,
7637 name: &'a str,
7638 #[serde(rename = "piece length")]
7639 piece_length: u64,
7640 #[serde(with = "serde_bytes")]
7641 pieces: &'a [u8],
7642 private: i64,
7643 }
7644
7645 #[derive(Serialize)]
7646 struct Torrent<'a> {
7647 announce: &'a str,
7648 info: Info<'a>,
7649 }
7650
7651 let data = vec![0xAB; 16384];
7652 let hash = irontide_core::sha1(&data);
7653 let mut pieces = Vec::new();
7654 pieces.extend_from_slice(hash.as_bytes());
7655
7656 let t = Torrent {
7657 announce: "http://private-tracker.example.com/announce",
7658 info: Info {
7659 length: data.len() as u64,
7660 name: "private_test",
7661 piece_length: 16384,
7662 pieces: &pieces,
7663 private: 1,
7664 },
7665 };
7666
7667 let bytes = irontide_bencode::to_bytes(&t).unwrap();
7668 let meta = torrent_from_bytes(&bytes).unwrap();
7669 assert_eq!(meta.info.private, Some(1));
7670
7671 let storage = make_storage(&data, 16384);
7672 let config = test_config();
7673
7674 let (atx, amask) = test_alert_channel();
7675 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7676 let handle = TorrentHandle::from_torrent(
7677 meta,
7678 irontide_core::TorrentVersion::V1Only,
7679 None,
7680 dh,
7681 dm,
7682 config,
7683 test_dht_rx(),
7684 test_dht_rx(),
7685 None,
7686 None,
7687 crate::slot_tuner::SlotTuner::disabled(4),
7688 atx,
7689 amask,
7690 None,
7691 None,
7692 test_ban_manager(),
7693 test_ip_filter(),
7694 Arc::new(Vec::new()),
7695 None,
7696 None,
7697 Arc::new(crate::transport::NetworkFactory::tokio()),
7698 None, Arc::new(crate::stats::SessionCounters::new()),
7700 )
7701 .await
7702 .unwrap();
7703
7704 let stats = handle.stats().await.unwrap();
7705 assert_eq!(stats.state, TorrentState::Downloading);
7706
7707 handle.shutdown().await.unwrap();
7708 }
7709
7710 #[tokio::test]
7713 async fn magnet_no_tracker_before_metadata() {
7714 let magnet = Magnet {
7715 info_hashes: irontide_core::InfoHashes::v1_only(
7716 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
7717 ),
7718 display_name: Some("magnet test".into()),
7719 trackers: vec![],
7720 peers: vec![],
7721 selected_files: None,
7722 };
7723
7724 let (atx, amask) = test_alert_channel();
7725 let (dm, _dj) = test_disk_manager();
7726 let handle = TorrentHandle::from_magnet(
7727 magnet,
7728 dm,
7729 test_config(),
7730 test_dht_rx(),
7731 test_dht_rx(),
7732 None,
7733 None,
7734 crate::slot_tuner::SlotTuner::disabled(4),
7735 atx,
7736 amask,
7737 None,
7738 None,
7739 test_ban_manager(),
7740 test_ip_filter(),
7741 Arc::new(Vec::new()),
7742 None,
7743 None,
7744 Arc::new(crate::transport::NetworkFactory::tokio()),
7745 None, Arc::new(crate::stats::SessionCounters::new()),
7747 )
7748 .await
7749 .unwrap();
7750
7751 let stats = handle.stats().await.unwrap();
7752 assert_eq!(stats.state, TorrentState::FetchingMetadata);
7753
7754 tokio::time::sleep(Duration::from_millis(50)).await;
7758
7759 handle.shutdown().await.unwrap();
7760 }
7761
7762 #[tokio::test]
7765 async fn pause_and_resume() {
7766 let data = vec![0xEEu8; 32768];
7767 let meta = make_test_torrent(&data, 16384);
7768 let storage = make_storage(&data, 16384);
7769 let config = test_config();
7770 let (atx, amask) = test_alert_channel();
7771 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7772 let handle = TorrentHandle::from_torrent(
7773 meta,
7774 irontide_core::TorrentVersion::V1Only,
7775 None,
7776 dh,
7777 dm,
7778 config,
7779 test_dht_rx(),
7780 test_dht_rx(),
7781 None,
7782 None,
7783 crate::slot_tuner::SlotTuner::disabled(4),
7784 atx,
7785 amask,
7786 None,
7787 None,
7788 test_ban_manager(),
7789 test_ip_filter(),
7790 Arc::new(Vec::new()),
7791 None,
7792 None,
7793 Arc::new(crate::transport::NetworkFactory::tokio()),
7794 None, Arc::new(crate::stats::SessionCounters::new()),
7796 )
7797 .await
7798 .unwrap();
7799
7800 let stats = handle.stats().await.unwrap();
7801 assert_eq!(stats.state, TorrentState::Downloading);
7802
7803 handle.pause().await.unwrap();
7804 tokio::time::sleep(Duration::from_millis(50)).await;
7805 let stats = handle.stats().await.unwrap();
7806 assert_eq!(stats.state, TorrentState::Paused);
7807
7808 handle.resume().await.unwrap();
7809 tokio::time::sleep(Duration::from_millis(50)).await;
7810 let stats = handle.stats().await.unwrap();
7811 assert_eq!(stats.state, TorrentState::Downloading);
7812
7813 handle.shutdown().await.unwrap();
7814 }
7815
7816 #[tokio::test]
7819 async fn pause_already_paused_is_noop() {
7820 let data = vec![0xEEu8; 32768];
7821 let meta = make_test_torrent(&data, 16384);
7822 let storage = make_storage(&data, 16384);
7823 let config = test_config();
7824 let (atx, amask) = test_alert_channel();
7825 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7826 let handle = TorrentHandle::from_torrent(
7827 meta,
7828 irontide_core::TorrentVersion::V1Only,
7829 None,
7830 dh,
7831 dm,
7832 config,
7833 test_dht_rx(),
7834 test_dht_rx(),
7835 None,
7836 None,
7837 crate::slot_tuner::SlotTuner::disabled(4),
7838 atx,
7839 amask,
7840 None,
7841 None,
7842 test_ban_manager(),
7843 test_ip_filter(),
7844 Arc::new(Vec::new()),
7845 None,
7846 None,
7847 Arc::new(crate::transport::NetworkFactory::tokio()),
7848 None, Arc::new(crate::stats::SessionCounters::new()),
7850 )
7851 .await
7852 .unwrap();
7853
7854 handle.pause().await.unwrap();
7855 tokio::time::sleep(Duration::from_millis(50)).await;
7856 handle.pause().await.unwrap(); tokio::time::sleep(Duration::from_millis(50)).await;
7858 let stats = handle.stats().await.unwrap();
7859 assert_eq!(stats.state, TorrentState::Paused);
7860
7861 handle.shutdown().await.unwrap();
7862 }
7863
7864 #[tokio::test]
7870 async fn incoming_request_served_from_storage() {
7871 let data = vec![0xABu8; 16384];
7872 let meta = make_test_torrent(&data, 16384);
7873 let info_hash = meta.info_hash;
7874 let storage = make_storage(&data, 16384);
7875
7876 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7877 let listen_addr = listener.local_addr().unwrap();
7878 drop(listener);
7879
7880 let config = TorrentConfig {
7881 listen_port: listen_addr.port(),
7882 ..test_config()
7883 };
7884
7885 let (atx, amask) = test_alert_channel();
7886 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7887 let handle = TorrentHandle::from_torrent(
7888 meta,
7889 irontide_core::TorrentVersion::V1Only,
7890 None,
7891 dh,
7892 dm,
7893 config,
7894 test_dht_rx(),
7895 test_dht_rx(),
7896 None,
7897 None,
7898 crate::slot_tuner::SlotTuner::disabled(4),
7899 atx,
7900 amask,
7901 None,
7902 None,
7903 test_ban_manager(),
7904 test_ip_filter(),
7905 Arc::new(Vec::new()),
7906 None,
7907 None,
7908 Arc::new(crate::transport::NetworkFactory::tokio()),
7909 None, Arc::new(crate::stats::SessionCounters::new()),
7911 )
7912 .await
7913 .unwrap();
7914
7915 tokio::time::sleep(Duration::from_millis(50)).await;
7916
7917 let seed_data = data.clone();
7919 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7920 let seeder_task = tokio::spawn(async move {
7921 let (reader, writer) = tokio::io::split(seed_stream);
7922 let mut writer = writer;
7923 let mut reader = reader;
7924
7925 let hs = Handshake::new(
7926 info_hash,
7927 Id20::from_hex("6666666666666666666666666666666666666666").unwrap(),
7928 );
7929 writer.write_all(&hs.to_bytes()).await.unwrap();
7930 writer.flush().await.unwrap();
7931 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7932 reader.read_exact(&mut hs_buf).await.unwrap();
7933
7934 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7935 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7936
7937 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
7939 let payload = ext_hs.to_bytes().unwrap();
7940 framed_write
7941 .send(Message::Extended { ext_id: 0, payload })
7942 .await
7943 .unwrap();
7944
7945 let mut bf = Bitfield::new(1);
7947 bf.set(0);
7948 framed_write
7949 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7950 .await
7951 .unwrap();
7952 framed_write.send(Message::Unchoke).await.unwrap();
7953
7954 while let Some(Ok(msg)) = framed_read.next().await {
7956 if let Message::Request {
7957 index,
7958 begin,
7959 length,
7960 } = msg
7961 {
7962 let start = begin as usize;
7963 let end = start + length as usize;
7964 framed_write
7965 .send(Message::Piece {
7966 index,
7967 begin,
7968 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
7969 data_1: Bytes::new(),
7970 })
7971 .await
7972 .unwrap();
7973 }
7974 }
7975 });
7976
7977 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
7979 loop {
7980 tokio::time::sleep(Duration::from_millis(100)).await;
7981 let stats = handle.stats().await.unwrap();
7982 if stats.pieces_have == 1 {
7983 break;
7984 }
7985 assert!(
7986 tokio::time::Instant::now() <= deadline,
7987 "piece download did not complete within 5s"
7988 );
7989 }
7990
7991 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7993 let expected_data = data.clone();
7994 let leecher_task = tokio::spawn(async move {
7995 let (reader, writer) = tokio::io::split(leech_stream);
7996 let mut writer = writer;
7997 let mut reader = reader;
7998
7999 let hs = Handshake::new(
8000 info_hash,
8001 Id20::from_hex("7777777777777777777777777777777777777777").unwrap(),
8002 );
8003 writer.write_all(&hs.to_bytes()).await.unwrap();
8004 writer.flush().await.unwrap();
8005 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8006 reader.read_exact(&mut hs_buf).await.unwrap();
8007
8008 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8009 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8010
8011 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
8013 let payload = ext_hs.to_bytes().unwrap();
8014 framed_write
8015 .send(Message::Extended { ext_id: 0, payload })
8016 .await
8017 .unwrap();
8018
8019 framed_write.send(Message::Interested).await.unwrap();
8021
8022 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
8023 loop {
8024 tokio::select! {
8025 msg = framed_read.next() => {
8026 match msg {
8027 Some(Ok(Message::Unchoke)) => { break; }
8028 Some(Ok(_)) => {}
8029 _ => panic!("connection closed before unchoke"),
8030 }
8031 }
8032 () = tokio::time::sleep_until(deadline) => {
8033 panic!("timed out waiting for unchoke");
8034 }
8035 }
8036 }
8037
8038 framed_write
8040 .send(Message::Request {
8041 index: 0,
8042 begin: 0,
8043 length: 16384,
8044 })
8045 .await
8046 .unwrap();
8047
8048 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
8050 loop {
8051 tokio::select! {
8052 msg = framed_read.next() => {
8053 match msg {
8054 Some(Ok(Message::Piece { index, begin, data_0, data_1 })) => {
8055 assert_eq!(index, 0);
8056 assert_eq!(begin, 0);
8057 let _ = &data_1; assert_eq!(data_0.as_ref(), expected_data.as_slice());
8059 return; }
8061 Some(Ok(_)) => {}
8062 Some(Err(e)) => panic!("error reading: {e}"),
8063 None => panic!("connection closed before piece"),
8064 }
8065 }
8066 () = tokio::time::sleep_until(deadline) => {
8067 panic!("timed out waiting for piece data");
8068 }
8069 }
8070 }
8071 });
8072
8073 let result = tokio::time::timeout(Duration::from_secs(20), leecher_task).await;
8075 match result {
8076 Ok(Ok(())) => {}
8077 Ok(Err(e)) => panic!("leecher task panicked: {e}"),
8078 Err(elapsed) => panic!("test timed out after {elapsed}"),
8079 }
8080
8081 let stats = handle.stats().await.unwrap();
8083 assert!(
8084 stats.uploaded > 0,
8085 "expected uploaded > 0, got {}",
8086 stats.uploaded
8087 );
8088
8089 handle.shutdown().await.unwrap();
8090 seeder_task.abort();
8091 }
8092
8093 #[tokio::test]
8096 async fn seed_ratio_limit_stops_torrent() {
8097 let data = vec![0xCCu8; 16384];
8100 let meta = make_test_torrent(&data, 16384);
8101 let info_hash = meta.info_hash;
8102 let storage = make_storage(&data, 16384);
8103
8104 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8105 let listen_addr = listener.local_addr().unwrap();
8106 drop(listener);
8107
8108 let config = TorrentConfig {
8109 listen_port: listen_addr.port(),
8110 seed_ratio_limit: Some(1.0),
8111 ..test_config()
8112 };
8113
8114 let (atx, amask) = test_alert_channel();
8115 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8116 let handle = TorrentHandle::from_torrent(
8117 meta,
8118 irontide_core::TorrentVersion::V1Only,
8119 None,
8120 dh,
8121 dm,
8122 config,
8123 test_dht_rx(),
8124 test_dht_rx(),
8125 None,
8126 None,
8127 crate::slot_tuner::SlotTuner::disabled(4),
8128 atx,
8129 amask,
8130 None,
8131 None,
8132 test_ban_manager(),
8133 test_ip_filter(),
8134 Arc::new(Vec::new()),
8135 None,
8136 None,
8137 Arc::new(crate::transport::NetworkFactory::tokio()),
8138 None, Arc::new(crate::stats::SessionCounters::new()),
8140 )
8141 .await
8142 .unwrap();
8143
8144 tokio::time::sleep(Duration::from_millis(50)).await;
8145
8146 let seed_data = data.clone();
8148 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8149 let seeder_task = tokio::spawn(async move {
8150 let (reader, writer) = tokio::io::split(seed_stream);
8151 let mut writer = writer;
8152 let mut reader = reader;
8153
8154 let hs = Handshake::new(
8155 info_hash,
8156 Id20::from_hex("8888888888888888888888888888888888888888").unwrap(),
8157 );
8158 writer.write_all(&hs.to_bytes()).await.unwrap();
8159 writer.flush().await.unwrap();
8160 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8161 reader.read_exact(&mut hs_buf).await.unwrap();
8162
8163 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8164 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8165
8166 let _msg = framed_read.next().await;
8167 let ext_hs = ExtHandshake::new();
8168 let payload = ext_hs.to_bytes().unwrap();
8169 framed_write
8170 .send(Message::Extended { ext_id: 0, payload })
8171 .await
8172 .unwrap();
8173
8174 let mut bf = Bitfield::new(1);
8175 bf.set(0);
8176 framed_write
8177 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
8178 .await
8179 .unwrap();
8180 framed_write.send(Message::Unchoke).await.unwrap();
8181
8182 while let Some(Ok(msg)) = framed_read.next().await {
8183 if let Message::Request {
8184 index,
8185 begin,
8186 length,
8187 } = msg
8188 {
8189 let start = begin as usize;
8190 let end = start + length as usize;
8191 framed_write
8192 .send(Message::Piece {
8193 index,
8194 begin,
8195 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
8196 data_1: Bytes::new(),
8197 })
8198 .await
8199 .unwrap();
8200 }
8201 }
8202 });
8203
8204 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
8206 loop {
8207 tokio::time::sleep(Duration::from_millis(100)).await;
8208 let stats = handle.stats().await.unwrap();
8209 if stats.state == TorrentState::Seeding {
8210 break;
8211 }
8212 assert!(
8213 tokio::time::Instant::now() <= deadline,
8214 "download did not complete within 5s"
8215 );
8216 }
8217
8218 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
8220 let leecher_task = tokio::spawn(async move {
8221 let (reader, writer) = tokio::io::split(leech_stream);
8222 let mut writer = writer;
8223 let mut reader = reader;
8224
8225 let hs = Handshake::new(
8226 info_hash,
8227 Id20::from_hex("9999999999999999999999999999999999999999").unwrap(),
8228 );
8229 writer.write_all(&hs.to_bytes()).await.unwrap();
8230 writer.flush().await.unwrap();
8231 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8232 reader.read_exact(&mut hs_buf).await.unwrap();
8233
8234 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
8235 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
8236
8237 let _msg = framed_read.next().await;
8238 let ext_hs = ExtHandshake::new();
8239 let payload = ext_hs.to_bytes().unwrap();
8240 framed_write
8241 .send(Message::Extended { ext_id: 0, payload })
8242 .await
8243 .unwrap();
8244
8245 framed_write.send(Message::Interested).await.unwrap();
8246
8247 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
8249 loop {
8250 tokio::select! {
8251 msg = framed_read.next() => {
8252 match msg {
8253 Some(Ok(Message::Unchoke)) => break,
8254 Some(Ok(_)) => {}
8255 _ => return, }
8257 }
8258 () = tokio::time::sleep_until(deadline) => return,
8259 }
8260 }
8261
8262 framed_write
8264 .send(Message::Request {
8265 index: 0,
8266 begin: 0,
8267 length: 16384,
8268 })
8269 .await
8270 .unwrap();
8271
8272 while let Some(Ok(_msg)) = framed_read.next().await {}
8274 });
8275
8276 let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
8278 loop {
8279 tokio::time::sleep(Duration::from_millis(100)).await;
8280 let stats = handle.stats().await.unwrap();
8281 if stats.state == TorrentState::Stopped {
8282 assert!(
8283 stats.uploaded >= 16384,
8284 "expected uploaded >= 16384, got {}",
8285 stats.uploaded
8286 );
8287 break;
8288 }
8289 if tokio::time::Instant::now() > deadline {
8290 let stats = handle.stats().await.unwrap();
8291 panic!(
8292 "expected Stopped, got {:?}, uploaded={}, downloaded={}",
8293 stats.state, stats.uploaded, stats.downloaded
8294 );
8295 }
8296 }
8297
8298 handle.shutdown().await.unwrap();
8299 seeder_task.abort();
8300 leecher_task.abort();
8301 }
8302
8303 #[tokio::test]
8306 async fn resume_with_seeded_storage() {
8307 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
8309 let storage = make_seeded_storage(&data, 16384);
8310 let config = test_config();
8311
8312 let (atx, amask) = test_alert_channel();
8313 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8314 let handle = TorrentHandle::from_torrent(
8315 meta,
8316 irontide_core::TorrentVersion::V1Only,
8317 None,
8318 dh,
8319 dm,
8320 config,
8321 test_dht_rx(),
8322 test_dht_rx(),
8323 None,
8324 None,
8325 crate::slot_tuner::SlotTuner::disabled(4),
8326 atx,
8327 amask,
8328 None,
8329 None,
8330 test_ban_manager(),
8331 test_ip_filter(),
8332 Arc::new(Vec::new()),
8333 None,
8334 None,
8335 Arc::new(crate::transport::NetworkFactory::tokio()),
8336 None, Arc::new(crate::stats::SessionCounters::new()),
8338 )
8339 .await
8340 .unwrap();
8341
8342 tokio::time::sleep(Duration::from_millis(100)).await;
8344
8345 let stats = handle.stats().await.unwrap();
8346 assert_eq!(
8347 stats.state,
8348 TorrentState::Seeding,
8349 "should start as seeder with all pieces verified"
8350 );
8351 assert_eq!(stats.pieces_have, 2);
8352 assert_eq!(stats.pieces_total, 2);
8353
8354 handle.shutdown().await.unwrap();
8355 }
8356
8357 #[tokio::test]
8360 async fn save_resume_data_captures_state() {
8361 let data = vec![0xAB; 32768];
8362 let meta = make_test_torrent(&data, 16384);
8363 let info_hash = meta.info_hash;
8364 let storage = make_storage(&data, 16384);
8365 let config = test_config();
8366
8367 let (atx, amask) = test_alert_channel();
8368 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8369 let handle = TorrentHandle::from_torrent(
8370 meta,
8371 irontide_core::TorrentVersion::V1Only,
8372 None,
8373 dh,
8374 dm,
8375 config,
8376 test_dht_rx(),
8377 test_dht_rx(),
8378 None,
8379 None,
8380 crate::slot_tuner::SlotTuner::disabled(4),
8381 atx,
8382 amask,
8383 None,
8384 None,
8385 test_ban_manager(),
8386 test_ip_filter(),
8387 Arc::new(Vec::new()),
8388 None,
8389 None,
8390 Arc::new(crate::transport::NetworkFactory::tokio()),
8391 None, Arc::new(crate::stats::SessionCounters::new()),
8393 )
8394 .await
8395 .unwrap();
8396
8397 tokio::time::sleep(Duration::from_millis(50)).await;
8399
8400 let rd = handle.save_resume_data().await.unwrap();
8401
8402 assert_eq!(rd.file_format, "libtorrent resume file");
8403 assert_eq!(rd.file_version, 1);
8404 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
8405 assert_eq!(rd.name, "test");
8406 assert_eq!(rd.save_path, "/tmp");
8407 assert_eq!(rd.paused, 0);
8408 assert!(!rd.pieces.is_empty());
8410 assert_eq!(rd.total_uploaded, 0);
8412 assert_eq!(rd.total_downloaded, 0);
8413
8414 handle.shutdown().await.unwrap();
8415 }
8416
8417 #[tokio::test]
8420 async fn save_resume_data_seeder() {
8421 let data = vec![0xCD; 32768];
8422 let meta = make_test_torrent(&data, 16384);
8423 let info_hash = meta.info_hash;
8424 let storage = make_seeded_storage(&data, 16384);
8425 let config = test_config();
8426
8427 let (atx, amask) = test_alert_channel();
8428 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8429 let handle = TorrentHandle::from_torrent(
8430 meta,
8431 irontide_core::TorrentVersion::V1Only,
8432 None,
8433 dh,
8434 dm,
8435 config,
8436 test_dht_rx(),
8437 test_dht_rx(),
8438 None,
8439 None,
8440 crate::slot_tuner::SlotTuner::disabled(4),
8441 atx,
8442 amask,
8443 None,
8444 None,
8445 test_ban_manager(),
8446 test_ip_filter(),
8447 Arc::new(Vec::new()),
8448 None,
8449 None,
8450 Arc::new(crate::transport::NetworkFactory::tokio()),
8451 None, Arc::new(crate::stats::SessionCounters::new()),
8453 )
8454 .await
8455 .unwrap();
8456
8457 tokio::time::sleep(Duration::from_millis(100)).await;
8459
8460 let rd = handle.save_resume_data().await.unwrap();
8461
8462 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
8463 assert_eq!(rd.name, "test");
8464 assert_eq!(rd.seed_mode, 1, "seeder should have seed_mode=1");
8465 assert_eq!(rd.paused, 0);
8466 assert_eq!(rd.pieces.len(), 1);
8469 assert_eq!(
8470 rd.pieces[0] & 0xC0,
8471 0xC0,
8472 "both pieces should be marked complete"
8473 );
8474
8475 handle.shutdown().await.unwrap();
8476 }
8477
8478 #[tokio::test]
8481 async fn save_resume_data_paused() {
8482 let data = vec![0xEF; 16384];
8483 let meta = make_test_torrent(&data, 16384);
8484 let storage = make_storage(&data, 16384);
8485 let config = test_config();
8486
8487 let (atx, amask) = test_alert_channel();
8488 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8489 let handle = TorrentHandle::from_torrent(
8490 meta,
8491 irontide_core::TorrentVersion::V1Only,
8492 None,
8493 dh,
8494 dm,
8495 config,
8496 test_dht_rx(),
8497 test_dht_rx(),
8498 None,
8499 None,
8500 crate::slot_tuner::SlotTuner::disabled(4),
8501 atx,
8502 amask,
8503 None,
8504 None,
8505 test_ban_manager(),
8506 test_ip_filter(),
8507 Arc::new(Vec::new()),
8508 None,
8509 None,
8510 Arc::new(crate::transport::NetworkFactory::tokio()),
8511 None, Arc::new(crate::stats::SessionCounters::new()),
8513 )
8514 .await
8515 .unwrap();
8516
8517 tokio::time::sleep(Duration::from_millis(50)).await;
8518 handle.pause().await.unwrap();
8519 tokio::time::sleep(Duration::from_millis(50)).await;
8520
8521 let rd = handle.save_resume_data().await.unwrap();
8522 assert_eq!(rd.paused, 1, "paused torrent should have paused=1");
8523 assert_eq!(rd.seed_mode, 0);
8524
8525 handle.shutdown().await.unwrap();
8526 }
8527
8528 #[tokio::test]
8531 async fn set_file_priority_and_read_back() {
8532 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
8533 let mut torrent_bytes = b"d4:info".to_vec();
8534 torrent_bytes.extend_from_slice(info_bytes);
8535 torrent_bytes.push(b'e');
8536
8537 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
8538 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
8539 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8540 let config = TorrentConfig {
8541 listen_port: 0,
8542 ..Default::default()
8543 };
8544
8545 let (atx, amask) = test_alert_channel();
8546 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8547 let handle = TorrentHandle::from_torrent(
8548 meta,
8549 irontide_core::TorrentVersion::V1Only,
8550 None,
8551 dh,
8552 dm,
8553 config,
8554 test_dht_rx(),
8555 test_dht_rx(),
8556 None,
8557 None,
8558 crate::slot_tuner::SlotTuner::disabled(4),
8559 atx,
8560 amask,
8561 None,
8562 None,
8563 test_ban_manager(),
8564 test_ip_filter(),
8565 Arc::new(Vec::new()),
8566 None,
8567 None,
8568 Arc::new(crate::transport::NetworkFactory::tokio()),
8569 None, Arc::new(crate::stats::SessionCounters::new()),
8571 )
8572 .await
8573 .unwrap();
8574
8575 let prios = handle.file_priorities().await.unwrap();
8577 assert_eq!(prios.len(), 2);
8578 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
8579
8580 handle
8582 .set_file_priority(0, FilePriority::Skip)
8583 .await
8584 .unwrap();
8585
8586 let prios = handle.file_priorities().await.unwrap();
8587 assert_eq!(prios[0], FilePriority::Skip);
8588 assert_eq!(prios[1], FilePriority::Normal);
8589
8590 let result = handle.set_file_priority(99, FilePriority::High).await;
8592 assert!(result.is_err());
8593
8594 handle.shutdown().await.unwrap();
8595 tokio::time::sleep(Duration::from_millis(50)).await;
8596 }
8597
8598 async fn spawn_test_torrent_multifile() -> TorrentHandle {
8602 let meta = make_multi_file_meta(&[(100, "a.bin"), (150, "b.bin"), (100, "c.bin")], 100);
8603 let lengths = Lengths::new(350, 100, DEFAULT_CHUNK_SIZE);
8604 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8605 let config = TorrentConfig {
8606 listen_port: 0,
8607 ..Default::default()
8608 };
8609 let (atx, amask) = test_alert_channel();
8610 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8611 TorrentHandle::from_torrent(
8612 meta,
8613 irontide_core::TorrentVersion::V1Only,
8614 None,
8615 dh,
8616 dm,
8617 config,
8618 test_dht_rx(),
8619 test_dht_rx(),
8620 None,
8621 None,
8622 crate::slot_tuner::SlotTuner::disabled(4),
8623 atx,
8624 amask,
8625 None,
8626 None,
8627 test_ban_manager(),
8628 test_ip_filter(),
8629 Arc::new(Vec::new()),
8630 None,
8631 None,
8632 Arc::new(crate::transport::NetworkFactory::tokio()),
8633 None,
8634 Arc::new(crate::stats::SessionCounters::new()),
8635 )
8636 .await
8637 .unwrap()
8638 }
8639
8640 #[tokio::test]
8646 async fn set_file_priority_updates_wanted_and_priorities() {
8647 let handle = spawn_test_torrent_multifile().await;
8648
8649 let prios = handle.file_priorities().await.unwrap();
8650 assert_eq!(prios.len(), 3);
8651 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
8652
8653 handle
8654 .set_file_priority(1, FilePriority::Skip)
8655 .await
8656 .unwrap();
8657 assert_eq!(
8658 handle.file_priorities().await.unwrap()[1],
8659 FilePriority::Skip
8660 );
8661
8662 handle
8663 .set_file_priority(1, FilePriority::Normal)
8664 .await
8665 .unwrap();
8666 assert_eq!(
8667 handle.file_priorities().await.unwrap()[1],
8668 FilePriority::Normal
8669 );
8670
8671 assert!(
8673 handle
8674 .set_file_priority(99, FilePriority::High)
8675 .await
8676 .is_err()
8677 );
8678
8679 handle.shutdown().await.unwrap();
8680 tokio::time::sleep(Duration::from_millis(50)).await;
8681 }
8682
8683 fn priority_test_actor(files: &[(u64, &str)], piece_length: u64) -> TorrentActor {
8689 use irontide_storage::Bitfield;
8690 let meta = make_multi_file_meta(files, piece_length);
8691 let total: u64 = files.iter().map(|(l, _)| *l).sum();
8692 let lengths = Lengths::new(total, piece_length, DEFAULT_CHUNK_SIZE);
8693 let num_pieces = lengths.num_pieces();
8694 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8695
8696 let mut actor = TorrentActor::for_throttle_test(num_pieces, 0);
8697 actor.file_priorities = vec![FilePriority::Normal; files.len()];
8698 actor.wanted_pieces = crate::piece_selector::build_wanted_pieces(
8699 &actor.file_priorities,
8700 &file_lengths,
8701 &lengths,
8702 );
8703 actor.cached_files = Some(build_cached_file_info(&meta, &lengths));
8704
8705 let we_have = Bitfield::new(num_pieces);
8706 actor.atomic_states = Some(Arc::new(crate::piece_reservation::AtomicPieceStates::new(
8707 num_pieces,
8708 &we_have,
8709 &actor.wanted_pieces,
8710 )));
8711 actor.piece_tracker = Some(crate::piece_reservation::PieceTracker::new(
8712 num_pieces,
8713 &we_have,
8714 &actor.wanted_pieces,
8715 ));
8716 actor.meta = Some(meta);
8717 actor.lengths = Some(lengths);
8718 actor
8719 }
8720
8721 #[tokio::test]
8726 async fn apply_file_priority_scoped_matches_full_rebuild() {
8727 let files: &[(u64, &str)] = &[(100, "a"), (150, "b"), (100, "c"), (250, "d")];
8728 let piece_length = 100;
8729 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8730 for skip_idx in 0..files.len() {
8731 let mut actor = priority_test_actor(files, piece_length);
8732 actor
8733 .apply_file_priority_scoped(skip_idx, FilePriority::Skip)
8734 .unwrap();
8735
8736 let mut ref_prios = vec![FilePriority::Normal; files.len()];
8737 ref_prios[skip_idx] = FilePriority::Skip;
8738 let reference = crate::piece_selector::build_wanted_pieces(
8739 &ref_prios,
8740 &file_lengths,
8741 actor.lengths.as_ref().unwrap(),
8742 );
8743 for p in 0..actor.num_pieces {
8744 assert_eq!(
8745 actor.wanted_pieces.get(p),
8746 reference.get(p),
8747 "piece {p} mismatch after scoped skip of file {skip_idx}"
8748 );
8749 }
8750 }
8751 }
8752
8753 #[tokio::test]
8757 async fn apply_file_priority_scoped_handles_sub_piece_files() {
8758 let files: &[(u64, &str)] = &[(30, "a"), (30, "b"), (40, "c"), (100, "d")];
8759 let mut actor = priority_test_actor(files, 100);
8760
8761 actor
8762 .apply_file_priority_scoped(0, FilePriority::Skip)
8763 .unwrap();
8764 assert!(
8765 actor.wanted_pieces.get(0),
8766 "piece 0 wanted: b,c still want it"
8767 );
8768 actor
8769 .apply_file_priority_scoped(1, FilePriority::Skip)
8770 .unwrap();
8771 actor
8772 .apply_file_priority_scoped(2, FilePriority::Skip)
8773 .unwrap();
8774 assert!(
8775 !actor.wanted_pieces.get(0),
8776 "piece 0 unwanted: a,b,c all skip"
8777 );
8778 assert!(
8779 actor.wanted_pieces.get(1),
8780 "piece 1 still wanted (d Normal)"
8781 );
8782
8783 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8784 let prios = vec![
8785 FilePriority::Skip,
8786 FilePriority::Skip,
8787 FilePriority::Skip,
8788 FilePriority::Normal,
8789 ];
8790 let reference = crate::piece_selector::build_wanted_pieces(
8791 &prios,
8792 &file_lengths,
8793 actor.lengths.as_ref().unwrap(),
8794 );
8795 for p in 0..actor.num_pieces {
8796 assert_eq!(
8797 actor.wanted_pieces.get(p),
8798 reference.get(p),
8799 "piece {p} mismatch"
8800 );
8801 }
8802 }
8803
8804 #[tokio::test]
8807 async fn apply_file_priority_scoped_zero_length_file_no_panic() {
8808 let files: &[(u64, &str)] = &[(100, "a"), (0, "empty"), (100, "c")];
8809 let mut actor = priority_test_actor(files, 100);
8810
8811 let r = actor
8812 .apply_file_priority_scoped(1, FilePriority::Skip)
8813 .unwrap();
8814 assert!(
8815 r.0 > r.1,
8816 "zero-length file yields an empty range, got {r:?}"
8817 );
8818
8819 actor
8820 .apply_file_priority_scoped(0, FilePriority::Skip)
8821 .unwrap();
8822
8823 let file_lengths: Vec<u64> = files.iter().map(|(l, _)| *l).collect();
8824 let prios = vec![FilePriority::Skip, FilePriority::Skip, FilePriority::Normal];
8825 let reference = crate::piece_selector::build_wanted_pieces(
8826 &prios,
8827 &file_lengths,
8828 actor.lengths.as_ref().unwrap(),
8829 );
8830 for p in 0..actor.num_pieces {
8831 assert_eq!(
8832 actor.wanted_pieces.get(p),
8833 reference.get(p),
8834 "piece {p} mismatch"
8835 );
8836 }
8837 }
8838
8839 #[tokio::test]
8842 async fn sync_piece_states_for_range_only_touches_range() {
8843 let files: &[(u64, &str)] = &[(200, "a"), (200, "b"), (200, "c")];
8844 let mut actor = priority_test_actor(files, 100); let (first, last) = actor
8846 .apply_file_priority_scoped(1, FilePriority::Skip)
8847 .unwrap();
8848 assert_eq!((first, last), (2, 3));
8849 actor.sync_piece_states_for_range(first, last);
8850
8851 let atomic = actor.atomic_states.as_ref().unwrap();
8852 assert_eq!(
8853 atomic.get(2),
8854 crate::piece_reservation::PieceState::Unwanted
8855 );
8856 assert_eq!(
8857 atomic.get(3),
8858 crate::piece_reservation::PieceState::Unwanted
8859 );
8860 for p in [0u32, 1, 4, 5] {
8861 assert_eq!(
8862 atomic.get(p),
8863 crate::piece_reservation::PieceState::Available,
8864 "piece {p} outside the range must be untouched"
8865 );
8866 }
8867 }
8868
8869 #[tokio::test]
8875 async fn order_map_coalesces_and_gen_is_monotone() {
8876 let mut actor = priority_test_actor(&[(200, "a"), (200, "b"), (200, "c")], 100);
8878 let gen0 = actor.order_map_tx.borrow().generation;
8879
8880 actor
8883 .apply_file_priority_scoped(0, FilePriority::Skip)
8884 .unwrap();
8885 actor.order_map_dirty = true;
8886 actor
8887 .apply_file_priority_scoped(2, FilePriority::Skip)
8888 .unwrap();
8889 actor.order_map_dirty = true;
8890 assert_eq!(
8891 actor.order_map_tx.borrow().generation,
8892 gen0,
8893 "no order-map rebuild before the tick"
8894 );
8895
8896 actor.rebuild_order_map_now();
8898 assert_eq!(
8899 actor.order_map_tx.borrow().generation,
8900 gen0 + 1,
8901 "exactly one coalesced rebuild"
8902 );
8903 assert!(!actor.order_map_dirty, "dirty flag cleared after rebuild");
8904
8905 let map = actor.order_map_tx.borrow();
8908 for p in [0u32, 1, 4, 5] {
8909 assert!(
8910 !map.order.contains(&p),
8911 "skipped piece {p} must be absent from the order"
8912 );
8913 }
8914 for p in [2u32, 3] {
8915 assert!(
8916 map.order.contains(&p),
8917 "wanted piece {p} must be present in the order"
8918 );
8919 }
8920 }
8921
8922 #[tokio::test]
8926 async fn rebuild_order_map_now_clears_dirty_flag() {
8927 let mut actor = priority_test_actor(&[(200, "a"), (200, "b")], 100);
8928 actor.order_map_dirty = true;
8929 actor.rebuild_order_map_now();
8930 assert!(!actor.order_map_dirty);
8931 }
8932
8933 #[tokio::test]
8934 async fn resume_data_preserves_file_priorities() {
8935 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
8936 let mut torrent_bytes = b"d4:info".to_vec();
8937 torrent_bytes.extend_from_slice(info_bytes);
8938 torrent_bytes.push(b'e');
8939
8940 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
8941 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
8942 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
8943 let config = TorrentConfig {
8944 listen_port: 0,
8945 ..Default::default()
8946 };
8947
8948 let (atx, amask) = test_alert_channel();
8949 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8950 let handle = TorrentHandle::from_torrent(
8951 meta,
8952 irontide_core::TorrentVersion::V1Only,
8953 None,
8954 dh,
8955 dm,
8956 config,
8957 test_dht_rx(),
8958 test_dht_rx(),
8959 None,
8960 None,
8961 crate::slot_tuner::SlotTuner::disabled(4),
8962 atx,
8963 amask,
8964 None,
8965 None,
8966 test_ban_manager(),
8967 test_ip_filter(),
8968 Arc::new(Vec::new()),
8969 None,
8970 None,
8971 Arc::new(crate::transport::NetworkFactory::tokio()),
8972 None, Arc::new(crate::stats::SessionCounters::new()),
8974 )
8975 .await
8976 .unwrap();
8977
8978 handle
8980 .set_file_priority(0, FilePriority::High)
8981 .await
8982 .unwrap();
8983 handle
8984 .set_file_priority(1, FilePriority::Skip)
8985 .await
8986 .unwrap();
8987
8988 let rd = handle.save_resume_data().await.unwrap();
8990 assert_eq!(rd.file_priority, vec![7, 0]); let encoded = irontide_bencode::to_bytes(&rd).unwrap();
8994 let decoded: irontide_core::FastResumeData =
8995 irontide_bencode::from_bytes(&encoded).unwrap();
8996 assert_eq!(decoded.file_priority, vec![7, 0]);
8997
8998 handle.shutdown().await.unwrap();
8999 tokio::time::sleep(Duration::from_millis(50)).await;
9000 }
9001
9002 #[tokio::test]
9005 async fn upload_rate_limiting_caps_throughput() {
9006 let data = vec![0xAB; 16384]; let meta = make_test_torrent(&data, 16384);
9012 let info_hash = meta.info_hash;
9013 let storage = make_seeded_storage(&data, 16384);
9014
9015 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9016 let listen_addr = listener.local_addr().unwrap();
9017
9018 let config = TorrentConfig {
9019 listen_port: listen_addr.port(),
9020 upload_rate_limit: 1024, ..test_config()
9022 };
9023
9024 drop(listener);
9025 let (atx, amask) = test_alert_channel();
9026 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9027 let handle = TorrentHandle::from_torrent(
9028 meta,
9029 irontide_core::TorrentVersion::V1Only,
9030 None,
9031 dh,
9032 dm,
9033 config,
9034 test_dht_rx(),
9035 test_dht_rx(),
9036 None,
9037 None,
9038 crate::slot_tuner::SlotTuner::disabled(4),
9039 atx,
9040 amask,
9041 None,
9042 None,
9043 test_ban_manager(),
9044 test_ip_filter(),
9045 Arc::new(Vec::new()),
9046 None,
9047 None,
9048 Arc::new(crate::transport::NetworkFactory::tokio()),
9049 None, Arc::new(crate::stats::SessionCounters::new()),
9051 )
9052 .await
9053 .unwrap();
9054
9055 tokio::time::sleep(Duration::from_millis(50)).await;
9056
9057 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
9059 let (reader, writer) = tokio::io::split(stream);
9060 let mut writer = writer;
9061 let mut reader = reader;
9062
9063 let hs = Handshake::new(
9064 info_hash,
9065 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
9066 );
9067 writer.write_all(&hs.to_bytes()).await.unwrap();
9068 writer.flush().await.unwrap();
9069 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
9070 reader.read_exact(&mut hs_buf).await.unwrap();
9071
9072 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
9073 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
9074
9075 let _msg = framed_read.next().await;
9077 let ext_hs = ExtHandshake::new();
9078 let payload = ext_hs.to_bytes().unwrap();
9079 framed_write
9080 .send(Message::Extended { ext_id: 0, payload })
9081 .await
9082 .unwrap();
9083
9084 let _bf_msg = framed_read.next().await;
9086
9087 framed_write.send(Message::Interested).await.unwrap();
9089
9090 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
9092 loop {
9093 tokio::select! {
9094 msg = framed_read.next() => {
9095 match msg {
9096 Some(Ok(Message::Unchoke)) => break,
9097 Some(Ok(_)) => {}
9098 _ => panic!("connection closed before unchoke"),
9099 }
9100 }
9101 () = tokio::time::sleep_until(deadline) => {
9102 panic!("timed out waiting for unchoke");
9103 }
9104 }
9105 }
9106
9107 framed_write
9109 .send(Message::Request {
9110 index: 0,
9111 begin: 0,
9112 length: 16384,
9113 })
9114 .await
9115 .unwrap();
9116
9117 let mut got_piece = false;
9121 if let Ok(true) = tokio::time::timeout(Duration::from_secs(2), async {
9122 loop {
9123 match framed_read.next().await {
9124 Some(Ok(Message::Piece { .. })) => return true,
9125 Some(Ok(_)) => {}
9126 _ => return false,
9127 }
9128 }
9129 })
9130 .await
9131 {
9132 got_piece = true;
9133 }
9134
9135 assert!(
9137 !got_piece,
9138 "piece should be delayed by rate limiter (1 KB/s for 16 KB chunk)"
9139 );
9140
9141 let stats = handle.stats().await.unwrap();
9143 assert_eq!(stats.uploaded, 0); handle.shutdown().await.unwrap();
9146 }
9147
9148 #[tokio::test]
9149 async fn unlimited_rate_has_no_effect() {
9150 let data = vec![0xAB; 32768];
9152 let meta = make_test_torrent(&data, 16384);
9153 let storage = make_storage(&data, 16384);
9154 let config = test_config();
9155
9156 assert_eq!(config.upload_rate_limit, 0);
9158 assert_eq!(config.download_rate_limit, 0);
9159
9160 let (atx, amask) = test_alert_channel();
9161 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9162 let handle = TorrentHandle::from_torrent(
9163 meta,
9164 irontide_core::TorrentVersion::V1Only,
9165 None,
9166 dh,
9167 dm,
9168 config,
9169 test_dht_rx(),
9170 test_dht_rx(),
9171 None,
9172 None,
9173 crate::slot_tuner::SlotTuner::disabled(4),
9174 atx,
9175 amask,
9176 None,
9177 None,
9178 test_ban_manager(),
9179 test_ip_filter(),
9180 Arc::new(Vec::new()),
9181 None,
9182 None,
9183 Arc::new(crate::transport::NetworkFactory::tokio()),
9184 None, Arc::new(crate::stats::SessionCounters::new()),
9186 )
9187 .await
9188 .unwrap();
9189
9190 let stats = handle.stats().await.unwrap();
9191 assert_eq!(stats.state, TorrentState::Downloading);
9192 assert_eq!(stats.pieces_total, 2);
9193
9194 handle.shutdown().await.unwrap();
9195 }
9196
9197 #[tokio::test]
9198 async fn download_rate_limiting_throttles_requests() {
9199 let data = vec![0xAB; 32768];
9202 let meta = make_test_torrent(&data, 16384);
9203 let info_hash = meta.info_hash;
9204 let storage = make_storage(&data, 16384);
9205
9206 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9207 let listen_addr = listener.local_addr().unwrap();
9208
9209 let config = TorrentConfig {
9210 listen_port: listen_addr.port(),
9211 download_rate_limit: 1024, ..test_config()
9213 };
9214
9215 drop(listener);
9216 let (atx, amask) = test_alert_channel();
9217 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9218 let handle = TorrentHandle::from_torrent(
9219 meta,
9220 irontide_core::TorrentVersion::V1Only,
9221 None,
9222 dh,
9223 dm,
9224 config,
9225 test_dht_rx(),
9226 test_dht_rx(),
9227 None,
9228 None,
9229 crate::slot_tuner::SlotTuner::disabled(4),
9230 atx,
9231 amask,
9232 None,
9233 None,
9234 test_ban_manager(),
9235 test_ip_filter(),
9236 Arc::new(Vec::new()),
9237 None,
9238 None,
9239 Arc::new(crate::transport::NetworkFactory::tokio()),
9240 None, Arc::new(crate::stats::SessionCounters::new()),
9242 )
9243 .await
9244 .unwrap();
9245
9246 tokio::time::sleep(Duration::from_millis(50)).await;
9247
9248 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
9250 let (reader, writer) = tokio::io::split(stream);
9251 let mut writer = writer;
9252 let mut reader = reader;
9253
9254 let hs = Handshake::new(
9255 info_hash,
9256 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
9257 );
9258 writer.write_all(&hs.to_bytes()).await.unwrap();
9259 writer.flush().await.unwrap();
9260 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
9261 reader.read_exact(&mut hs_buf).await.unwrap();
9262
9263 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
9264 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
9265
9266 let _msg = framed_read.next().await;
9268 let ext_hs = ExtHandshake::new();
9269 let payload = ext_hs.to_bytes().unwrap();
9270 framed_write
9271 .send(Message::Extended { ext_id: 0, payload })
9272 .await
9273 .unwrap();
9274
9275 let mut bf = Bitfield::new(2);
9277 bf.set(0);
9278 bf.set(1);
9279 framed_write
9280 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
9281 .await
9282 .unwrap();
9283
9284 framed_write.send(Message::Unchoke).await.unwrap();
9286
9287 let mut requests_received = 0u32;
9291 let deadline = tokio::time::Instant::now() + Duration::from_millis(500);
9292 loop {
9293 match tokio::time::timeout(
9294 deadline.saturating_duration_since(tokio::time::Instant::now()),
9295 framed_read.next(),
9296 )
9297 .await
9298 {
9299 Ok(Some(Ok(Message::Request { .. }))) => {
9300 requests_received += 1;
9301 }
9302 Ok(Some(Ok(_))) => {}
9303 _ => break,
9304 }
9305 }
9306
9307 let stats = handle.stats().await.unwrap();
9308 assert_eq!(stats.state, TorrentState::Downloading);
9309
9310 assert!(
9313 requests_received <= 2,
9314 "with 1 KB/s limit, should get very few requests, got {requests_received}"
9315 );
9316
9317 handle.shutdown().await.unwrap();
9318 }
9319
9320 #[test]
9323 fn piece_contributor_tracking() {
9324 use std::net::IpAddr;
9325 let mut contributors: HashMap<u32, HashSet<IpAddr>> = HashMap::new();
9326 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9327 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9328
9329 contributors.entry(0).or_default().insert(ip1);
9330 contributors.entry(0).or_default().insert(ip2);
9331 assert_eq!(contributors[&0].len(), 2);
9332 assert!(contributors[&0].contains(&ip1));
9333 assert!(contributors[&0].contains(&ip2));
9334
9335 contributors.remove(&0);
9337 assert!(!contributors.contains_key(&0));
9338 }
9339
9340 #[test]
9341 fn parole_enter_on_hash_failure() {
9342 use crate::ban::ParoleState;
9343 use std::net::IpAddr;
9344
9345 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9346 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9347 let contributors = vec![ip1, ip2];
9348
9349 let parole = ParoleState {
9351 original_contributors: contributors.into_iter().collect(),
9352 parole_peer: None,
9353 };
9354
9355 assert_eq!(parole.original_contributors.len(), 2);
9356 assert!(parole.original_contributors.contains(&ip1));
9357 assert!(parole.original_contributors.contains(&ip2));
9358 assert!(parole.parole_peer.is_none());
9359 }
9360
9361 #[test]
9362 fn parole_success_strikes_originals() {
9363 use crate::ban::{BanConfig, BanManager, ParoleState};
9364 use std::net::IpAddr;
9365
9366 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9367 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
9368 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
9369
9370 let mut mgr = BanManager::new(BanConfig {
9371 max_failures: 2,
9372 use_parole: true,
9373 });
9374
9375 let parole = ParoleState {
9376 original_contributors: [ip1, ip2].into_iter().collect(),
9377 parole_peer: Some(parole_ip),
9378 };
9379
9380 for ip in &parole.original_contributors {
9382 mgr.record_strike(*ip);
9383 }
9384
9385 assert_eq!(*mgr.strikes_map().get(&ip1).unwrap(), 1);
9386 assert_eq!(*mgr.strikes_map().get(&ip2).unwrap(), 1);
9387 assert!(!mgr.strikes_map().contains_key(&parole_ip));
9389
9390 for ip in &parole.original_contributors {
9392 mgr.record_strike(*ip);
9393 }
9394 assert!(mgr.is_banned(&ip1));
9395 assert!(mgr.is_banned(&ip2));
9396 }
9397
9398 #[test]
9399 fn parole_failure_strikes_parole_peer() {
9400 use crate::ban::{BanConfig, BanManager, ParoleState};
9401 use std::net::IpAddr;
9402
9403 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
9404 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
9405
9406 let mut mgr = BanManager::new(BanConfig {
9407 max_failures: 2,
9408 use_parole: true,
9409 });
9410
9411 let parole = ParoleState {
9412 original_contributors: [ip1].into_iter().collect(),
9413 parole_peer: Some(parole_ip),
9414 };
9415
9416 if let Some(pp) = parole.parole_peer {
9418 mgr.record_strike(pp);
9419 }
9420
9421 assert_eq!(*mgr.strikes_map().get(&parole_ip).unwrap(), 1);
9422 assert!(!mgr.strikes_map().contains_key(&ip1));
9423 }
9424
9425 #[tokio::test]
9426 async fn banned_peer_rejected_on_connect() {
9427 let data = vec![0xAB; 32768];
9428 let meta = make_test_torrent(&data, 16384);
9429 let storage = make_storage(&data, 16384);
9430 let config = test_config();
9431 let ban_mgr = test_ban_manager();
9432
9433 let banned_ip: std::net::IpAddr = "192.168.1.100".parse().unwrap();
9435 ban_mgr.write().ban(banned_ip);
9436
9437 let (atx, amask) = test_alert_channel();
9438 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9439 let handle = TorrentHandle::from_torrent(
9440 meta,
9441 irontide_core::TorrentVersion::V1Only,
9442 None,
9443 dh,
9444 dm,
9445 config,
9446 test_dht_rx(),
9447 test_dht_rx(),
9448 None,
9449 None,
9450 crate::slot_tuner::SlotTuner::disabled(4),
9451 atx,
9452 amask,
9453 None,
9454 None,
9455 Arc::clone(&ban_mgr),
9456 test_ip_filter(),
9457 Arc::new(Vec::new()),
9458 None,
9459 None,
9460 Arc::new(crate::transport::NetworkFactory::tokio()),
9461 None, Arc::new(crate::stats::SessionCounters::new()),
9463 )
9464 .await
9465 .unwrap();
9466
9467 handle
9469 .add_peers(
9470 vec![
9471 SocketAddr::new(banned_ip, 6881),
9472 "10.0.0.1:6881".parse().unwrap(),
9473 ],
9474 PeerSource::Tracker,
9475 )
9476 .await
9477 .unwrap();
9478
9479 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9480 let stats = handle.stats().await.unwrap();
9481 assert!(
9484 stats.peers_available + stats.peers_connected <= 1,
9485 "banned peer should not be added: available={}, connected={}",
9486 stats.peers_available,
9487 stats.peers_connected
9488 );
9489
9490 handle.shutdown().await.unwrap();
9491 }
9492
9493 #[test]
9494 fn banned_peer_filtered_from_available() {
9495 use crate::ban::{BanConfig, BanManager};
9496 use std::net::IpAddr;
9497
9498 let banned_ip: IpAddr = "192.168.1.200".parse().unwrap();
9499 let ok_ip: IpAddr = "10.0.0.1".parse().unwrap();
9500
9501 let mgr = BanManager::new(BanConfig::default());
9502 assert!(!mgr.is_banned(&banned_ip));
9504 assert!(!mgr.is_banned(&ok_ip));
9505
9506 let mut mgr = BanManager::new(BanConfig::default());
9507 mgr.ban(banned_ip);
9508
9509 assert!(mgr.is_banned(&banned_ip));
9511 assert!(!mgr.is_banned(&ok_ip));
9512 }
9513
9514 #[test]
9517 fn hashing_threads_config_default() {
9518 let s = irontide_settings::Settings::default();
9519 let expected = {
9520 let cores = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
9521 (cores / 4).clamp(2, 8)
9522 };
9523 assert_eq!(s.hashing_threads, expected);
9524 let tc = TorrentConfig::default();
9525 assert_eq!(tc.hashing_threads, expected);
9526 }
9527
9528 #[tokio::test]
9529 async fn checking_state_and_progress_alerts() {
9530 use crate::alert::AlertKind;
9531
9532 let data = vec![0xEEu8; 65536]; let meta = make_test_torrent(&data, 16384);
9534 let storage = make_seeded_storage(&data, 16384);
9535 let config = test_config();
9536
9537 let (atx, amask) = test_alert_channel();
9538 let mut rx = atx.subscribe();
9539 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9540 let handle = TorrentHandle::from_torrent(
9541 meta,
9542 irontide_core::TorrentVersion::V1Only,
9543 None,
9544 dh,
9545 dm,
9546 config,
9547 test_dht_rx(),
9548 test_dht_rx(),
9549 None,
9550 None,
9551 crate::slot_tuner::SlotTuner::disabled(4),
9552 atx,
9553 amask,
9554 None,
9555 None,
9556 test_ban_manager(),
9557 test_ip_filter(),
9558 Arc::new(Vec::new()),
9559 None,
9560 None,
9561 Arc::new(crate::transport::NetworkFactory::tokio()),
9562 None, Arc::new(crate::stats::SessionCounters::new()),
9564 )
9565 .await
9566 .unwrap();
9567
9568 let mut saw_checking = false;
9570 let mut progress_values: Vec<f32> = Vec::new();
9571 let mut saw_checked = false;
9572 let mut checked_have = 0u32;
9573 let mut checked_total = 0u32;
9574
9575 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
9576 while tokio::time::Instant::now() < deadline {
9577 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
9578 Ok(Ok(alert)) => match alert.kind {
9579 AlertKind::StateChanged {
9580 new_state: TorrentState::Checking,
9581 ..
9582 } => {
9583 saw_checking = true;
9584 }
9585 AlertKind::CheckingProgress { progress, .. } => {
9586 progress_values.push(progress);
9587 }
9588 AlertKind::TorrentChecked {
9589 pieces_have,
9590 pieces_total,
9591 ..
9592 } => {
9593 saw_checked = true;
9594 checked_have = pieces_have;
9595 checked_total = pieces_total;
9596 break;
9597 }
9598 _ => {}
9599 },
9600 _ => break,
9601 }
9602 }
9603
9604 assert!(saw_checking, "should have seen StateChanged → Checking");
9605 assert!(
9606 !progress_values.is_empty(),
9607 "should have seen CheckingProgress alerts"
9608 );
9609 for w in progress_values.windows(2) {
9611 assert!(
9612 w[1] >= w[0],
9613 "progress should be monotonically increasing: {} < {}",
9614 w[0],
9615 w[1]
9616 );
9617 }
9618 assert!(saw_checked, "should have seen TorrentChecked");
9619 assert_eq!(checked_have, 4);
9620 assert_eq!(checked_total, 4);
9621
9622 tokio::time::sleep(Duration::from_millis(50)).await;
9624 let stats = handle.stats().await.unwrap();
9625 assert_eq!(stats.state, TorrentState::Seeding);
9626
9627 handle.shutdown().await.unwrap();
9628 }
9629
9630 #[tokio::test]
9631 #[allow(clippy::float_cmp, reason = "exact sentinel value comparison (0.0)")]
9632 async fn checking_progress_in_stats() {
9633 let data = vec![0xAB; 32768];
9635 let meta = make_test_torrent(&data, 16384);
9636 let storage = make_storage(&data, 16384);
9637 let config = test_config();
9638
9639 let (atx, amask) = test_alert_channel();
9640 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9641 let handle = TorrentHandle::from_torrent(
9642 meta,
9643 irontide_core::TorrentVersion::V1Only,
9644 None,
9645 dh,
9646 dm,
9647 config,
9648 test_dht_rx(),
9649 test_dht_rx(),
9650 None,
9651 None,
9652 crate::slot_tuner::SlotTuner::disabled(4),
9653 atx,
9654 amask,
9655 None,
9656 None,
9657 test_ban_manager(),
9658 test_ip_filter(),
9659 Arc::new(Vec::new()),
9660 None,
9661 None,
9662 Arc::new(crate::transport::NetworkFactory::tokio()),
9663 None, Arc::new(crate::stats::SessionCounters::new()),
9665 )
9666 .await
9667 .unwrap();
9668
9669 tokio::time::sleep(Duration::from_millis(100)).await;
9671
9672 let stats = handle.stats().await.unwrap();
9673 assert_eq!(stats.state, TorrentState::Downloading);
9674 assert_eq!(
9675 stats.checking_progress, 0.0,
9676 "checking_progress should be 0.0 when not checking"
9677 );
9678
9679 handle.shutdown().await.unwrap();
9680 }
9681
9682 #[tokio::test]
9683 async fn verify_pieces_partial_data() {
9684 use crate::alert::AlertKind;
9685
9686 let data = vec![0xCCu8; 65536]; let meta = make_test_torrent(&data, 16384);
9689
9690 let lengths = Lengths::new(data.len() as u64, 16384, DEFAULT_CHUNK_SIZE);
9692 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
9693 for p in 0..2u32 {
9694 let offset = lengths.piece_offset(p) as usize;
9695 let size = lengths.piece_size(p) as usize;
9696 storage
9697 .write_chunk(p, 0, &data[offset..offset + size])
9698 .unwrap();
9699 }
9700 let config = test_config();
9703 let (atx, amask) = test_alert_channel();
9704 let mut rx = atx.subscribe();
9705 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9706 let handle = TorrentHandle::from_torrent(
9707 meta,
9708 irontide_core::TorrentVersion::V1Only,
9709 None,
9710 dh,
9711 dm,
9712 config,
9713 test_dht_rx(),
9714 test_dht_rx(),
9715 None,
9716 None,
9717 crate::slot_tuner::SlotTuner::disabled(4),
9718 atx,
9719 amask,
9720 None,
9721 None,
9722 test_ban_manager(),
9723 test_ip_filter(),
9724 Arc::new(Vec::new()),
9725 None,
9726 None,
9727 Arc::new(crate::transport::NetworkFactory::tokio()),
9728 None, Arc::new(crate::stats::SessionCounters::new()),
9730 )
9731 .await
9732 .unwrap();
9733
9734 let mut checked_have = 0u32;
9736 let mut checked_total = 0u32;
9737 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
9738 while tokio::time::Instant::now() < deadline {
9739 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
9740 Ok(Ok(alert)) => {
9741 if let AlertKind::TorrentChecked {
9742 pieces_have,
9743 pieces_total,
9744 ..
9745 } = alert.kind
9746 {
9747 checked_have = pieces_have;
9748 checked_total = pieces_total;
9749 break;
9750 }
9751 }
9752 _ => break,
9753 }
9754 }
9755
9756 assert_eq!(checked_have, 2, "only 2 pieces should be valid");
9757 assert_eq!(checked_total, 4);
9758
9759 tokio::time::sleep(Duration::from_millis(50)).await;
9761 let stats = handle.stats().await.unwrap();
9762 assert_eq!(stats.state, TorrentState::Downloading);
9763 assert_eq!(stats.pieces_have, 2);
9764 assert_eq!(stats.pieces_total, 4);
9765
9766 handle.shutdown().await.unwrap();
9767 }
9768
9769 #[tokio::test]
9772 async fn ip_filter_blocks_peers_in_handle_add_peers() {
9773 let data = vec![0xCD; 32768];
9774 let meta = make_test_torrent(&data, 16384);
9775 let storage = make_storage(&data, 16384);
9776 let config = test_config();
9777
9778 let ip_filter = {
9780 let mut f = crate::ip_filter::IpFilter::new();
9781 f.add_rule(
9782 "203.0.113.0".parse().unwrap(),
9783 "203.0.113.255".parse().unwrap(),
9784 1,
9785 );
9786 Arc::new(parking_lot::RwLock::new(f))
9787 };
9788
9789 let (atx, amask) = test_alert_channel();
9790 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9791 let handle = TorrentHandle::from_torrent(
9792 meta,
9793 irontide_core::TorrentVersion::V1Only,
9794 None,
9795 dh,
9796 dm,
9797 config,
9798 test_dht_rx(),
9799 test_dht_rx(),
9800 None,
9801 None,
9802 crate::slot_tuner::SlotTuner::disabled(4),
9803 atx,
9804 amask,
9805 None,
9806 None,
9807 test_ban_manager(),
9808 Arc::clone(&ip_filter),
9809 Arc::new(Vec::new()),
9810 None,
9811 None,
9812 Arc::new(crate::transport::NetworkFactory::tokio()),
9813 None, Arc::new(crate::stats::SessionCounters::new()),
9815 )
9816 .await
9817 .unwrap();
9818
9819 let blocked_addr: SocketAddr = "203.0.113.42:6881".parse().unwrap();
9821 let allowed_addr: SocketAddr = "198.51.100.1:6881".parse().unwrap();
9822 handle
9823 .add_peers(vec![blocked_addr, allowed_addr], PeerSource::Tracker)
9824 .await
9825 .unwrap();
9826
9827 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9828 let stats = handle.stats().await.unwrap();
9829 assert!(
9831 stats.peers_available + stats.peers_connected <= 1,
9832 "blocked peer should not be added: available={}, connected={}",
9833 stats.peers_available,
9834 stats.peers_connected
9835 );
9836
9837 handle.shutdown().await.unwrap();
9838 }
9839
9840 #[tokio::test]
9841 async fn set_ip_filter_replaces_filter_and_blocks_new_ip() {
9842 let data = vec![0xCD; 32768];
9845 let meta = make_test_torrent(&data, 16384);
9846 let storage = make_storage(&data, 16384);
9847 let config = test_config();
9848
9849 let ip_filter: irontide_session_types::SharedIpFilter =
9851 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
9852
9853 let (atx, amask) = test_alert_channel();
9854 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9855 let handle = TorrentHandle::from_torrent(
9856 meta,
9857 irontide_core::TorrentVersion::V1Only,
9858 None,
9859 dh,
9860 dm,
9861 config,
9862 test_dht_rx(),
9863 test_dht_rx(),
9864 None,
9865 None,
9866 crate::slot_tuner::SlotTuner::disabled(4),
9867 atx,
9868 amask,
9869 None,
9870 None,
9871 test_ban_manager(),
9872 Arc::clone(&ip_filter),
9873 Arc::new(Vec::new()),
9874 None,
9875 None,
9876 Arc::new(crate::transport::NetworkFactory::tokio()),
9877 None, Arc::new(crate::stats::SessionCounters::new()),
9879 )
9880 .await
9881 .unwrap();
9882
9883 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
9886 let local_addr = listener.local_addr().unwrap();
9887 handle
9888 .add_peers(vec![local_addr], PeerSource::Tracker)
9889 .await
9890 .unwrap();
9891 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
9892 let stats = handle.stats().await.unwrap();
9893 assert!(
9894 stats.peers_available + stats.peers_connected >= 1,
9895 "peer should be allowed initially"
9896 );
9897 handle.shutdown().await.unwrap();
9898
9899 {
9901 let mut f = ip_filter.write();
9902 f.add_rule(
9903 "198.51.100.0".parse().unwrap(),
9904 "198.51.100.255".parse().unwrap(),
9905 1,
9906 );
9907 }
9908
9909 assert!(ip_filter.read().is_blocked("198.51.100.1".parse().unwrap()));
9911 assert!(!ip_filter.read().is_blocked("203.0.113.1".parse().unwrap()));
9913 }
9914
9915 #[test]
9916 fn relocate_files_moves_and_cleans_up() {
9917 let tmp = std::env::temp_dir().join(format!("torrent_relocate_{}", std::process::id()));
9918 let src = tmp.join("src");
9919 let dst = tmp.join("dst");
9920
9921 let subdir = src.join("TorrentName").join("subdir");
9925 std::fs::create_dir_all(&subdir).unwrap();
9926 std::fs::write(subdir.join("file1.txt"), b"hello").unwrap();
9927 std::fs::write(src.join("TorrentName").join("file2.txt"), b"world").unwrap();
9928
9929 let file_paths = vec![
9930 std::path::PathBuf::from("TorrentName/subdir/file1.txt"),
9931 std::path::PathBuf::from("TorrentName/file2.txt"),
9932 ];
9933
9934 relocate_files(&src, &dst, &file_paths).unwrap();
9935
9936 assert_eq!(
9938 std::fs::read_to_string(dst.join("TorrentName/subdir/file1.txt")).unwrap(),
9939 "hello"
9940 );
9941 assert_eq!(
9942 std::fs::read_to_string(dst.join("TorrentName/file2.txt")).unwrap(),
9943 "world"
9944 );
9945
9946 assert!(!src.join("TorrentName").join("subdir").exists());
9948 assert!(!src.join("TorrentName").exists());
9949
9950 let _ = std::fs::remove_dir_all(&tmp);
9952 }
9953
9954 #[test]
9955 fn relocate_files_skips_missing() {
9956 let tmp =
9957 std::env::temp_dir().join(format!("torrent_relocate_skip_{}", std::process::id()));
9958 let src = tmp.join("src");
9959 let dst = tmp.join("dst");
9960 std::fs::create_dir_all(&src).unwrap();
9961
9962 let file_paths = vec![std::path::PathBuf::from("nonexistent.txt")];
9964 relocate_files(&src, &dst, &file_paths).unwrap();
9965
9966 assert!(!dst.join("nonexistent.txt").exists());
9967
9968 let _ = std::fs::remove_dir_all(&tmp);
9969 }
9970
9971 #[tokio::test]
9974 async fn force_recheck_transitions_to_checking() {
9975 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
9977 let storage = make_seeded_storage(&data, 16384);
9978 let config = test_config();
9979
9980 let (atx, amask) = test_alert_channel();
9981 let mut arx = atx.subscribe();
9982 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9983 let handle = TorrentHandle::from_torrent(
9984 meta,
9985 irontide_core::TorrentVersion::V1Only,
9986 None,
9987 dh,
9988 dm,
9989 config,
9990 test_dht_rx(),
9991 test_dht_rx(),
9992 None,
9993 None,
9994 crate::slot_tuner::SlotTuner::disabled(4),
9995 atx,
9996 amask,
9997 None,
9998 None,
9999 test_ban_manager(),
10000 test_ip_filter(),
10001 Arc::new(Vec::new()),
10002 None,
10003 None,
10004 Arc::new(crate::transport::NetworkFactory::tokio()),
10005 None, Arc::new(crate::stats::SessionCounters::new()),
10007 )
10008 .await
10009 .unwrap();
10010
10011 tokio::time::sleep(Duration::from_millis(100)).await;
10013 let stats = handle.stats().await.unwrap();
10014 assert_eq!(stats.state, TorrentState::Seeding, "should start as seeder");
10015
10016 while arx.try_recv().is_ok() {}
10018
10019 handle.force_recheck().await.unwrap();
10021
10022 let mut saw_checking = false;
10025 while let Ok(alert) = arx.try_recv() {
10026 if let crate::alert::AlertKind::StateChanged { new_state, .. } = alert.kind
10027 && new_state == TorrentState::Checking
10028 {
10029 saw_checking = true;
10030 }
10031 }
10032 assert!(
10033 saw_checking,
10034 "should have transitioned through Checking state"
10035 );
10036
10037 handle.shutdown().await.unwrap();
10038 }
10039
10040 #[tokio::test]
10043 async fn force_recheck_completes() {
10044 let data = vec![0xEEu8; 32768]; let meta = make_test_torrent(&data, 16384);
10046 let storage = make_seeded_storage(&data, 16384);
10047 let config = test_config();
10048
10049 let (atx, amask) = test_alert_channel();
10050 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10051 let handle = TorrentHandle::from_torrent(
10052 meta,
10053 irontide_core::TorrentVersion::V1Only,
10054 None,
10055 dh,
10056 dm,
10057 config,
10058 test_dht_rx(),
10059 test_dht_rx(),
10060 None,
10061 None,
10062 crate::slot_tuner::SlotTuner::disabled(4),
10063 atx,
10064 amask,
10065 None,
10066 None,
10067 test_ban_manager(),
10068 test_ip_filter(),
10069 Arc::new(Vec::new()),
10070 None,
10071 None,
10072 Arc::new(crate::transport::NetworkFactory::tokio()),
10073 None, Arc::new(crate::stats::SessionCounters::new()),
10075 )
10076 .await
10077 .unwrap();
10078
10079 tokio::time::sleep(Duration::from_millis(100)).await;
10081 let stats = handle.stats().await.unwrap();
10082 assert_eq!(stats.state, TorrentState::Seeding);
10083 assert_eq!(stats.pieces_have, 2);
10084
10085 handle.force_recheck().await.unwrap();
10087
10088 let stats = handle.stats().await.unwrap();
10089 assert_eq!(
10090 stats.state,
10091 TorrentState::Seeding,
10092 "should return to Seeding after recheck"
10093 );
10094 assert_eq!(stats.pieces_have, 2, "all pieces should still be verified");
10095
10096 handle.shutdown().await.unwrap();
10097 }
10098
10099 #[tokio::test]
10102 async fn rename_file_succeeds() {
10103 let tmp = std::env::temp_dir().join(format!("torrent_rename_{}", std::process::id()));
10105 std::fs::create_dir_all(&tmp).unwrap();
10106
10107 let data = vec![0xFFu8; 16384]; let meta = make_test_torrent(&data, 16384);
10109 let storage = make_seeded_storage(&data, 16384);
10110
10111 std::fs::write(tmp.join("test"), &data).unwrap();
10114
10115 let mut config = test_config();
10116 config.download_dir = tmp.clone();
10117
10118 let (atx, amask) = test_alert_channel();
10119 let mut arx = atx.subscribe();
10120 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10121 let handle = TorrentHandle::from_torrent(
10122 meta,
10123 irontide_core::TorrentVersion::V1Only,
10124 None,
10125 dh,
10126 dm,
10127 config,
10128 test_dht_rx(),
10129 test_dht_rx(),
10130 None,
10131 None,
10132 crate::slot_tuner::SlotTuner::disabled(4),
10133 atx,
10134 amask,
10135 None,
10136 None,
10137 test_ban_manager(),
10138 test_ip_filter(),
10139 Arc::new(Vec::new()),
10140 None,
10141 None,
10142 Arc::new(crate::transport::NetworkFactory::tokio()),
10143 None, Arc::new(crate::stats::SessionCounters::new()),
10145 )
10146 .await
10147 .unwrap();
10148
10149 tokio::time::sleep(Duration::from_millis(100)).await;
10151
10152 while arx.try_recv().is_ok() {}
10154
10155 handle.rename_file(0, "test_renamed".into()).await.unwrap();
10157
10158 assert!(!tmp.join("test").exists(), "old file should be removed");
10160 assert!(tmp.join("test_renamed").exists(), "new file should exist");
10161
10162 let mut saw_renamed = false;
10164 while let Ok(alert) = arx.try_recv() {
10165 if let AlertKind::FileRenamed { index, .. } = alert.kind {
10166 assert_eq!(index, 0);
10167 saw_renamed = true;
10168 }
10169 }
10170 assert!(saw_renamed, "should have received FileRenamed alert");
10171
10172 handle.shutdown().await.unwrap();
10173 let _ = std::fs::remove_dir_all(&tmp);
10174 }
10175
10176 #[tokio::test]
10179 async fn rename_file_invalid_index_errors() {
10180 let data = vec![0xCCu8; 16384]; let meta = make_test_torrent(&data, 16384);
10182 let storage = make_seeded_storage(&data, 16384);
10183 let config = test_config();
10184
10185 let (atx, amask) = test_alert_channel();
10186 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10187 let handle = TorrentHandle::from_torrent(
10188 meta,
10189 irontide_core::TorrentVersion::V1Only,
10190 None,
10191 dh,
10192 dm,
10193 config,
10194 test_dht_rx(),
10195 test_dht_rx(),
10196 None,
10197 None,
10198 crate::slot_tuner::SlotTuner::disabled(4),
10199 atx,
10200 amask,
10201 None,
10202 None,
10203 test_ban_manager(),
10204 test_ip_filter(),
10205 Arc::new(Vec::new()),
10206 None,
10207 None,
10208 Arc::new(crate::transport::NetworkFactory::tokio()),
10209 None, Arc::new(crate::stats::SessionCounters::new()),
10211 )
10212 .await
10213 .unwrap();
10214
10215 tokio::time::sleep(Duration::from_millis(100)).await;
10217
10218 let result = handle.rename_file(99, "bad".into()).await;
10220 assert!(result.is_err(), "should fail for out-of-range file index");
10221
10222 handle.shutdown().await.unwrap();
10223 }
10224
10225 #[tokio::test]
10228 async fn file_completed_alert_fires() {
10229 let data = vec![0xBBu8; 32768]; let meta = make_test_torrent(&data, 16384);
10231 let storage = make_seeded_storage(&data, 16384);
10232 let config = test_config();
10233
10234 let (atx, amask) = test_alert_channel();
10235 let mut arx = atx.subscribe();
10236 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10237 let handle = TorrentHandle::from_torrent(
10238 meta,
10239 irontide_core::TorrentVersion::V1Only,
10240 None,
10241 dh,
10242 dm,
10243 config,
10244 test_dht_rx(),
10245 test_dht_rx(),
10246 None,
10247 None,
10248 crate::slot_tuner::SlotTuner::disabled(4),
10249 atx,
10250 amask,
10251 None,
10252 None,
10253 test_ban_manager(),
10254 test_ip_filter(),
10255 Arc::new(Vec::new()),
10256 None,
10257 None,
10258 Arc::new(crate::transport::NetworkFactory::tokio()),
10259 None, Arc::new(crate::stats::SessionCounters::new()),
10261 )
10262 .await
10263 .unwrap();
10264
10265 tokio::time::sleep(Duration::from_millis(200)).await;
10267
10268 let mut saw_file_completed = false;
10270 while let Ok(alert) = arx.try_recv() {
10271 if let AlertKind::FileCompleted { file_index, .. } = alert.kind {
10272 assert_eq!(file_index, 0, "should be file index 0");
10273 saw_file_completed = true;
10274 }
10275 }
10276 assert!(
10277 saw_file_completed,
10278 "should have received FileCompleted alert"
10279 );
10280
10281 handle.shutdown().await.unwrap();
10282 }
10283
10284 #[test]
10287 fn metadata_failed_alert_fires() {
10288 let info_hash = Id20::from([0u8; 20]);
10290 let alert = crate::alert::Alert::new(AlertKind::MetadataFailed { info_hash });
10291 assert!(
10292 alert
10293 .category()
10294 .contains(crate::alert::AlertCategory::STATUS),
10295 "MetadataFailed should have STATUS category"
10296 );
10297 assert!(
10298 alert
10299 .category()
10300 .contains(crate::alert::AlertCategory::ERROR),
10301 "MetadataFailed should have ERROR category"
10302 );
10303
10304 let (tx, mut rx) = broadcast::channel(16);
10306 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
10307 post_alert(&tx, &mask, AlertKind::MetadataFailed { info_hash });
10308 let received = rx.try_recv().expect("should receive MetadataFailed alert");
10309 assert!(matches!(received.kind, AlertKind::MetadataFailed { .. }));
10310 }
10311
10312 #[tokio::test]
10315 async fn set_max_connections_persists() {
10316 let data = vec![0xAB; 32768];
10317 let meta = make_test_torrent(&data, 16384);
10318 let storage = make_storage(&data, 16384);
10319 let config = test_config();
10320
10321 let (atx, amask) = test_alert_channel();
10322 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10323 let handle = TorrentHandle::from_torrent(
10324 meta,
10325 irontide_core::TorrentVersion::V1Only,
10326 None,
10327 dh,
10328 dm,
10329 config,
10330 test_dht_rx(),
10331 test_dht_rx(),
10332 None,
10333 None,
10334 crate::slot_tuner::SlotTuner::disabled(4),
10335 atx,
10336 amask,
10337 None,
10338 None,
10339 test_ban_manager(),
10340 test_ip_filter(),
10341 Arc::new(Vec::new()),
10342 None,
10343 None,
10344 Arc::new(crate::transport::NetworkFactory::tokio()),
10345 None, Arc::new(crate::stats::SessionCounters::new()),
10347 )
10348 .await
10349 .unwrap();
10350
10351 handle.set_max_connections(10).await.unwrap();
10353 let val = handle.max_connections().await.unwrap();
10354 assert_eq!(val, 10);
10355
10356 handle.set_max_connections(25).await.unwrap();
10358 let val = handle.max_connections().await.unwrap();
10359 assert_eq!(val, 25);
10360
10361 let stats = handle.stats().await.unwrap();
10363 assert_eq!(stats.connections_limit, 25);
10364
10365 handle.shutdown().await.unwrap();
10366 }
10367
10368 #[tokio::test]
10371 async fn max_connections_default() {
10372 let data = vec![0xAB; 32768];
10373 let meta = make_test_torrent(&data, 16384);
10374 let storage = make_storage(&data, 16384);
10375 let config = test_config();
10376 let expected_default = config.max_peers;
10377
10378 let (atx, amask) = test_alert_channel();
10379 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10380 let handle = TorrentHandle::from_torrent(
10381 meta,
10382 irontide_core::TorrentVersion::V1Only,
10383 None,
10384 dh,
10385 dm,
10386 config,
10387 test_dht_rx(),
10388 test_dht_rx(),
10389 None,
10390 None,
10391 crate::slot_tuner::SlotTuner::disabled(4),
10392 atx,
10393 amask,
10394 None,
10395 None,
10396 test_ban_manager(),
10397 test_ip_filter(),
10398 Arc::new(Vec::new()),
10399 None,
10400 None,
10401 Arc::new(crate::transport::NetworkFactory::tokio()),
10402 None, Arc::new(crate::stats::SessionCounters::new()),
10404 )
10405 .await
10406 .unwrap();
10407
10408 let val = handle.max_connections().await.unwrap();
10410 assert_eq!(val, 0);
10411
10412 let stats = handle.stats().await.unwrap();
10414 assert_eq!(stats.connections_limit, expected_default);
10415
10416 handle.shutdown().await.unwrap();
10417 }
10418
10419 #[tokio::test]
10422 async fn set_max_uploads_round_trip() {
10423 let data = vec![0xAB; 32768];
10424 let meta = make_test_torrent(&data, 16384);
10425 let storage = make_storage(&data, 16384);
10426 let config = test_config();
10427
10428 let (atx, amask) = test_alert_channel();
10429 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10430 let handle = TorrentHandle::from_torrent(
10431 meta,
10432 irontide_core::TorrentVersion::V1Only,
10433 None,
10434 dh,
10435 dm,
10436 config,
10437 test_dht_rx(),
10438 test_dht_rx(),
10439 None,
10440 None,
10441 crate::slot_tuner::SlotTuner::disabled(4),
10442 atx,
10443 amask,
10444 None,
10445 None,
10446 test_ban_manager(),
10447 test_ip_filter(),
10448 Arc::new(Vec::new()),
10449 None,
10450 None,
10451 Arc::new(crate::transport::NetworkFactory::tokio()),
10452 None, Arc::new(crate::stats::SessionCounters::new()),
10454 )
10455 .await
10456 .unwrap();
10457
10458 handle.set_max_uploads(8).await.unwrap();
10460 let val = handle.max_uploads().await.unwrap();
10461 assert_eq!(val, 8);
10462
10463 let stats = handle.stats().await.unwrap();
10465 assert_eq!(stats.uploads_limit, 8);
10466
10467 handle.shutdown().await.unwrap();
10468 }
10469
10470 #[tokio::test]
10473 async fn external_ip_detected_alert() {
10474 let data = vec![0xAB; 32768];
10475 let meta = make_test_torrent(&data, 16384);
10476 let storage = make_storage(&data, 16384);
10477 let config = test_config();
10478
10479 let (atx, amask) = test_alert_channel();
10480 let mut arx = atx.subscribe();
10481 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10482 let handle = TorrentHandle::from_torrent(
10483 meta,
10484 irontide_core::TorrentVersion::V1Only,
10485 None,
10486 dh,
10487 dm,
10488 config,
10489 test_dht_rx(),
10490 test_dht_rx(),
10491 None,
10492 None,
10493 crate::slot_tuner::SlotTuner::disabled(4),
10494 atx,
10495 amask,
10496 None,
10497 None,
10498 test_ban_manager(),
10499 test_ip_filter(),
10500 Arc::new(Vec::new()),
10501 None,
10502 None,
10503 Arc::new(crate::transport::NetworkFactory::tokio()),
10504 None, Arc::new(crate::stats::SessionCounters::new()),
10506 )
10507 .await
10508 .unwrap();
10509
10510 while arx.try_recv().is_ok() {}
10512
10513 let test_ip: std::net::IpAddr = "203.0.113.42".parse().unwrap();
10515 handle
10516 .cmd_tx
10517 .send(TorrentCommand::UpdateExternalIp { ip: test_ip })
10518 .await
10519 .unwrap();
10520
10521 tokio::time::sleep(Duration::from_millis(50)).await;
10523
10524 let mut saw_alert = false;
10526 while let Ok(alert) = arx.try_recv() {
10527 if let AlertKind::ExternalIpDetected { ip } = alert.kind {
10528 assert_eq!(ip, test_ip);
10529 saw_alert = true;
10530 }
10531 }
10532 assert!(saw_alert, "should have received ExternalIpDetected alert");
10533
10534 handle.shutdown().await.unwrap();
10535 }
10536
10537 #[tokio::test]
10540 async fn get_peer_info_returns_connected_peers() {
10541 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
10544 let config = test_config();
10545
10546 let (atx, amask) = test_alert_channel();
10547 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10548 let handle = TorrentHandle::from_torrent(
10549 meta.clone(),
10550 irontide_core::TorrentVersion::V1Only,
10551 None,
10552 dh,
10553 dm,
10554 config,
10555 test_dht_rx(),
10556 test_dht_rx(),
10557 None,
10558 None,
10559 crate::slot_tuner::SlotTuner::disabled(4),
10560 atx,
10561 amask,
10562 None,
10563 None,
10564 test_ban_manager(),
10565 test_ip_filter(),
10566 Arc::new(Vec::new()),
10567 None,
10568 None,
10569 Arc::new(crate::transport::NetworkFactory::tokio()),
10570 None, Arc::new(crate::stats::SessionCounters::new()),
10572 )
10573 .await
10574 .unwrap();
10575
10576 let stats = handle.stats().await.unwrap();
10578 let listen_port = stats.peers_connected; let peer_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
10582 let peer_addr = peer_listener.local_addr().unwrap();
10583
10584 handle
10585 .add_peers(vec![peer_addr], PeerSource::Tracker)
10586 .await
10587 .unwrap();
10588
10589 let accept_timeout =
10591 tokio::time::timeout(Duration::from_secs(2), peer_listener.accept()).await;
10592 if let Ok(Ok((mut stream, _))) = accept_timeout {
10593 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
10595 if tokio::time::timeout(Duration::from_millis(500), stream.read_exact(&mut hs_buf))
10596 .await
10597 .is_ok()
10598 {
10599 let hs = Handshake::new(meta.info_hash, Id20::from([0xBB; 20]));
10601 let hs_bytes = hs.to_bytes();
10602 let _ = stream.write_all(&hs_bytes).await;
10603
10604 tokio::time::sleep(Duration::from_millis(200)).await;
10606
10607 let peer_info = handle.get_peer_info().await.unwrap();
10609 if !peer_info.is_empty() {
10611 let p = &peer_info[0];
10612 assert!(p.peer_choking, "peer should be choking us initially");
10614 assert!(
10616 !p.am_choking,
10617 "we should not be choking peer after connect (M107 unconditional unchoke)"
10618 );
10619 assert!(
10620 !p.peer_interested,
10621 "peer should not be interested initially"
10622 );
10623 assert_eq!(p.num_pieces, 0);
10624 assert_eq!(p.source, PeerSource::Tracker);
10625 }
10626 }
10627 }
10628 let _ = handle.get_peer_info().await.unwrap();
10630 assert_eq!(listen_port, 0); handle.shutdown().await.unwrap();
10633 }
10634
10635 #[tokio::test]
10638 async fn get_peer_info_empty_when_no_peers() {
10639 let data = vec![0xAB; 32768];
10640 let meta = make_test_torrent(&data, 16384);
10641 let storage = make_storage(&data, 16384);
10642 let config = test_config();
10643
10644 let (atx, amask) = test_alert_channel();
10645 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10646 let handle = TorrentHandle::from_torrent(
10647 meta,
10648 irontide_core::TorrentVersion::V1Only,
10649 None,
10650 dh,
10651 dm,
10652 config,
10653 test_dht_rx(),
10654 test_dht_rx(),
10655 None,
10656 None,
10657 crate::slot_tuner::SlotTuner::disabled(4),
10658 atx,
10659 amask,
10660 None,
10661 None,
10662 test_ban_manager(),
10663 test_ip_filter(),
10664 Arc::new(Vec::new()),
10665 None,
10666 None,
10667 Arc::new(crate::transport::NetworkFactory::tokio()),
10668 None, Arc::new(crate::stats::SessionCounters::new()),
10670 )
10671 .await
10672 .unwrap();
10673
10674 let peer_info = handle.get_peer_info().await.unwrap();
10675 assert!(peer_info.is_empty(), "should have no peers initially");
10676
10677 handle.shutdown().await.unwrap();
10678 }
10679
10680 #[tokio::test]
10683 async fn get_download_queue_empty_initially() {
10684 let data = vec![0xAB; 32768];
10685 let meta = make_test_torrent(&data, 16384);
10686 let storage = make_storage(&data, 16384);
10687 let config = test_config();
10688
10689 let (atx, amask) = test_alert_channel();
10690 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10691 let handle = TorrentHandle::from_torrent(
10692 meta,
10693 irontide_core::TorrentVersion::V1Only,
10694 None,
10695 dh,
10696 dm,
10697 config,
10698 test_dht_rx(),
10699 test_dht_rx(),
10700 None,
10701 None,
10702 crate::slot_tuner::SlotTuner::disabled(4),
10703 atx,
10704 amask,
10705 None,
10706 None,
10707 test_ban_manager(),
10708 test_ip_filter(),
10709 Arc::new(Vec::new()),
10710 None,
10711 None,
10712 Arc::new(crate::transport::NetworkFactory::tokio()),
10713 None, Arc::new(crate::stats::SessionCounters::new()),
10715 )
10716 .await
10717 .unwrap();
10718
10719 let queue = handle.get_download_queue().await.unwrap();
10720 assert!(
10721 queue.is_empty(),
10722 "download queue should be empty with no active downloads"
10723 );
10724
10725 handle.shutdown().await.unwrap();
10726 }
10727
10728 #[tokio::test]
10731 async fn have_piece_false_initially() {
10732 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10734 let storage = make_storage(&data, 16384);
10735 let config = test_config();
10736
10737 let (atx, amask) = test_alert_channel();
10738 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10739 let handle = TorrentHandle::from_torrent(
10740 meta,
10741 irontide_core::TorrentVersion::V1Only,
10742 None,
10743 dh,
10744 dm,
10745 config,
10746 test_dht_rx(),
10747 test_dht_rx(),
10748 None,
10749 None,
10750 crate::slot_tuner::SlotTuner::disabled(4),
10751 atx,
10752 amask,
10753 None,
10754 None,
10755 test_ban_manager(),
10756 test_ip_filter(),
10757 Arc::new(Vec::new()),
10758 None,
10759 None,
10760 Arc::new(crate::transport::NetworkFactory::tokio()),
10761 None, Arc::new(crate::stats::SessionCounters::new()),
10763 )
10764 .await
10765 .unwrap();
10766
10767 assert!(
10768 !handle.have_piece(0).await.unwrap(),
10769 "piece 0 should not be downloaded initially"
10770 );
10771 assert!(
10772 !handle.have_piece(1).await.unwrap(),
10773 "piece 1 should not be downloaded initially"
10774 );
10775
10776 handle.shutdown().await.unwrap();
10777 }
10778
10779 #[tokio::test]
10782 async fn piece_availability_empty_no_peers() {
10783 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10785 let storage = make_storage(&data, 16384);
10786 let config = test_config();
10787
10788 let (atx, amask) = test_alert_channel();
10789 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10790 let handle = TorrentHandle::from_torrent(
10791 meta,
10792 irontide_core::TorrentVersion::V1Only,
10793 None,
10794 dh,
10795 dm,
10796 config,
10797 test_dht_rx(),
10798 test_dht_rx(),
10799 None,
10800 None,
10801 crate::slot_tuner::SlotTuner::disabled(4),
10802 atx,
10803 amask,
10804 None,
10805 None,
10806 test_ban_manager(),
10807 test_ip_filter(),
10808 Arc::new(Vec::new()),
10809 None,
10810 None,
10811 Arc::new(crate::transport::NetworkFactory::tokio()),
10812 None, Arc::new(crate::stats::SessionCounters::new()),
10814 )
10815 .await
10816 .unwrap();
10817
10818 let avail = handle.piece_availability().await.unwrap();
10819 assert_eq!(avail.len(), 2, "should have availability for 2 pieces");
10820 assert!(
10821 avail.iter().all(|&c| c == 0),
10822 "all availability counts should be 0 with no peers"
10823 );
10824
10825 handle.shutdown().await.unwrap();
10826 }
10827
10828 #[tokio::test]
10831 async fn file_progress_zeros_initially() {
10832 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
10834 let storage = make_storage(&data, 16384);
10835 let config = test_config();
10836
10837 let (atx, amask) = test_alert_channel();
10838 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10839 let handle = TorrentHandle::from_torrent(
10840 meta,
10841 irontide_core::TorrentVersion::V1Only,
10842 None,
10843 dh,
10844 dm,
10845 config,
10846 test_dht_rx(),
10847 test_dht_rx(),
10848 None,
10849 None,
10850 crate::slot_tuner::SlotTuner::disabled(4),
10851 atx,
10852 amask,
10853 None,
10854 None,
10855 test_ban_manager(),
10856 test_ip_filter(),
10857 Arc::new(Vec::new()),
10858 None,
10859 None,
10860 Arc::new(crate::transport::NetworkFactory::tokio()),
10861 None, Arc::new(crate::stats::SessionCounters::new()),
10863 )
10864 .await
10865 .unwrap();
10866
10867 let progress = handle.file_progress().await.unwrap();
10868 assert_eq!(progress.len(), 1, "single-file torrent should have 1 entry");
10869 assert_eq!(progress[0], 0, "no bytes should be downloaded initially");
10870
10871 handle.shutdown().await.unwrap();
10872 }
10873
10874 fn make_test_torrent_multi(
10878 data: &[u8],
10879 piece_length: u64,
10880 file_lengths: &[u64],
10881 ) -> TorrentMetaV1 {
10882 use serde::Serialize;
10883
10884 #[derive(Serialize)]
10885 struct FileE {
10886 length: u64,
10887 path: Vec<String>,
10888 }
10889
10890 #[derive(Serialize)]
10891 struct Info<'a> {
10892 name: &'a str,
10893 #[serde(rename = "piece length")]
10894 piece_length: u64,
10895 #[serde(with = "serde_bytes")]
10896 pieces: &'a [u8],
10897 files: Vec<FileE>,
10898 }
10899
10900 #[derive(Serialize)]
10901 struct Torrent<'a> {
10902 info: Info<'a>,
10903 }
10904
10905 let mut pieces = Vec::new();
10906 let mut offset = 0;
10907 while offset < data.len() {
10908 let end = (offset + piece_length as usize).min(data.len());
10909 let hash = irontide_core::sha1(&data[offset..end]);
10910 pieces.extend_from_slice(hash.as_bytes());
10911 offset = end;
10912 }
10913
10914 let files: Vec<FileE> = file_lengths
10915 .iter()
10916 .enumerate()
10917 .map(|(i, &len)| FileE {
10918 length: len,
10919 path: vec![format!("file{i}.bin")],
10920 })
10921 .collect();
10922
10923 let t = Torrent {
10924 info: Info {
10925 name: "test_multi",
10926 piece_length,
10927 pieces: &pieces,
10928 files,
10929 },
10930 };
10931
10932 let bytes = irontide_bencode::to_bytes(&t).unwrap();
10933 torrent_from_bytes(&bytes).unwrap()
10934 }
10935
10936 #[tokio::test]
10937 async fn file_progress_length_matches_file_count() {
10938 let data = vec![0xCD; 32768];
10940 let file_lengths = [10000u64, 20000, 2768];
10941 let meta = make_test_torrent_multi(&data, 16384, &file_lengths);
10942 let storage = make_storage(&data, 16384);
10943 let config = test_config();
10944
10945 let (atx, amask) = test_alert_channel();
10946 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10947 let handle = TorrentHandle::from_torrent(
10948 meta,
10949 irontide_core::TorrentVersion::V1Only,
10950 None,
10951 dh,
10952 dm,
10953 config,
10954 test_dht_rx(),
10955 test_dht_rx(),
10956 None,
10957 None,
10958 crate::slot_tuner::SlotTuner::disabled(4),
10959 atx,
10960 amask,
10961 None,
10962 None,
10963 test_ban_manager(),
10964 test_ip_filter(),
10965 Arc::new(Vec::new()),
10966 None,
10967 None,
10968 Arc::new(crate::transport::NetworkFactory::tokio()),
10969 None, Arc::new(crate::stats::SessionCounters::new()),
10971 )
10972 .await
10973 .unwrap();
10974
10975 let progress = handle.file_progress().await.unwrap();
10976 assert_eq!(
10977 progress.len(),
10978 3,
10979 "multi-file torrent should have 3 entries"
10980 );
10981 assert!(
10982 progress.iter().all(|&b| b == 0),
10983 "all progress should be 0 initially"
10984 );
10985
10986 handle.shutdown().await.unwrap();
10987 }
10988
10989 #[tokio::test]
10992 async fn is_valid_true_for_active() {
10993 let data = vec![0xAB; 32768];
10994 let meta = make_test_torrent(&data, 16384);
10995 let storage = make_storage(&data, 16384);
10996 let config = test_config();
10997
10998 let (atx, amask) = test_alert_channel();
10999 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
11000 let handle = TorrentHandle::from_torrent(
11001 meta,
11002 irontide_core::TorrentVersion::V1Only,
11003 None,
11004 dh,
11005 dm,
11006 config,
11007 test_dht_rx(),
11008 test_dht_rx(),
11009 None,
11010 None,
11011 crate::slot_tuner::SlotTuner::disabled(4),
11012 atx,
11013 amask,
11014 None,
11015 None,
11016 test_ban_manager(),
11017 test_ip_filter(),
11018 Arc::new(Vec::new()),
11019 None,
11020 None,
11021 Arc::new(crate::transport::NetworkFactory::tokio()),
11022 None, Arc::new(crate::stats::SessionCounters::new()),
11024 )
11025 .await
11026 .unwrap();
11027
11028 assert!(
11029 handle.is_valid(),
11030 "handle should be valid while torrent actor is alive"
11031 );
11032
11033 handle.shutdown().await.unwrap();
11034 }
11035
11036 #[tokio::test]
11039 async fn is_valid_false_after_remove() {
11040 let data = vec![0xAB; 32768];
11041 let meta = make_test_torrent(&data, 16384);
11042 let storage = make_storage(&data, 16384);
11043 let config = test_config();
11044
11045 let (atx, amask) = test_alert_channel();
11046 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
11047 let handle = TorrentHandle::from_torrent(
11048 meta,
11049 irontide_core::TorrentVersion::V1Only,
11050 None,
11051 dh,
11052 dm,
11053 config,
11054 test_dht_rx(),
11055 test_dht_rx(),
11056 None,
11057 None,
11058 crate::slot_tuner::SlotTuner::disabled(4),
11059 atx,
11060 amask,
11061 None,
11062 None,
11063 test_ban_manager(),
11064 test_ip_filter(),
11065 Arc::new(Vec::new()),
11066 None,
11067 None,
11068 Arc::new(crate::transport::NetworkFactory::tokio()),
11069 None, Arc::new(crate::stats::SessionCounters::new()),
11071 )
11072 .await
11073 .unwrap();
11074
11075 assert!(handle.is_valid());
11076
11077 handle.shutdown().await.unwrap();
11079
11080 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
11082
11083 assert!(
11084 !handle.is_valid(),
11085 "handle should be invalid after shutdown"
11086 );
11087 }
11088
11089 #[tokio::test]
11092 async fn clear_error_resets() {
11093 let data = vec![0xAB; 32768];
11094 let meta = make_test_torrent(&data, 16384);
11095 let storage = make_storage(&data, 16384);
11096 let config = test_config();
11097
11098 let (atx, amask) = test_alert_channel();
11099 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
11100 let handle = TorrentHandle::from_torrent(
11101 meta,
11102 irontide_core::TorrentVersion::V1Only,
11103 None,
11104 dh,
11105 dm,
11106 config,
11107 test_dht_rx(),
11108 test_dht_rx(),
11109 None,
11110 None,
11111 crate::slot_tuner::SlotTuner::disabled(4),
11112 atx,
11113 amask,
11114 None,
11115 None,
11116 test_ban_manager(),
11117 test_ip_filter(),
11118 Arc::new(Vec::new()),
11119 None,
11120 None,
11121 Arc::new(crate::transport::NetworkFactory::tokio()),
11122 None, Arc::new(crate::stats::SessionCounters::new()),
11124 )
11125 .await
11126 .unwrap();
11127
11128 let stats = handle.stats().await.unwrap();
11130 assert!(stats.error.is_empty());
11131 assert_eq!(stats.error_file, -1);
11132
11133 handle.clear_error().await.unwrap();
11135
11136 let stats = handle.stats().await.unwrap();
11137 assert!(stats.error.is_empty());
11138 assert_eq!(stats.error_file, -1);
11139
11140 handle.shutdown().await.unwrap();
11141 }
11142
11143 #[tokio::test]
11146 async fn flags_round_trip() {
11147 let data = vec![0xAB; 32768];
11148 let meta = make_test_torrent(&data, 16384);
11149 let storage = make_storage(&data, 16384);
11150 let config = test_config();
11151
11152 let (atx, amask) = test_alert_channel();
11153 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
11154 let handle = TorrentHandle::from_torrent(
11155 meta,
11156 irontide_core::TorrentVersion::V1Only,
11157 None,
11158 dh,
11159 dm,
11160 config,
11161 test_dht_rx(),
11162 test_dht_rx(),
11163 None,
11164 None,
11165 crate::slot_tuner::SlotTuner::disabled(4),
11166 atx,
11167 amask,
11168 None,
11169 None,
11170 test_ban_manager(),
11171 test_ip_filter(),
11172 Arc::new(Vec::new()),
11173 None,
11174 None,
11175 Arc::new(crate::transport::NetworkFactory::tokio()),
11176 None, Arc::new(crate::stats::SessionCounters::new()),
11178 )
11179 .await
11180 .unwrap();
11181
11182 let initial = handle.flags().await.unwrap();
11184 assert!(!initial.contains(crate::types::TorrentFlags::PAUSED));
11185 assert!(!initial.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
11186 assert!(!initial.contains(crate::types::TorrentFlags::SUPER_SEEDING));
11187
11188 handle
11190 .set_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
11191 .await
11192 .unwrap();
11193 let after_set = handle.flags().await.unwrap();
11194 assert!(after_set.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
11195
11196 handle
11198 .unset_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
11199 .await
11200 .unwrap();
11201 let after_unset = handle.flags().await.unwrap();
11202 assert!(!after_unset.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
11203
11204 assert!(!handle.is_sequential_download().await.unwrap());
11206
11207 handle.shutdown().await.unwrap();
11208 }
11209
11210 #[tokio::test]
11213 async fn connect_peer_no_error() {
11214 let data = vec![0xAB; 32768];
11215 let meta = make_test_torrent(&data, 16384);
11216 let storage = make_storage(&data, 16384);
11217 let config = test_config();
11218
11219 let (atx, amask) = test_alert_channel();
11220 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
11221 let handle = TorrentHandle::from_torrent(
11222 meta,
11223 irontide_core::TorrentVersion::V1Only,
11224 None,
11225 dh,
11226 dm,
11227 config,
11228 test_dht_rx(),
11229 test_dht_rx(),
11230 None,
11231 None,
11232 crate::slot_tuner::SlotTuner::disabled(4),
11233 atx,
11234 amask,
11235 None,
11236 None,
11237 test_ban_manager(),
11238 test_ip_filter(),
11239 Arc::new(Vec::new()),
11240 None,
11241 None,
11242 Arc::new(crate::transport::NetworkFactory::tokio()),
11243 None, Arc::new(crate::stats::SessionCounters::new()),
11245 )
11246 .await
11247 .unwrap();
11248
11249 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
11252 handle.connect_peer(addr).await.unwrap();
11253
11254 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
11256
11257 handle.shutdown().await.unwrap();
11258 }
11259
11260 fn make_test_meta_v2(
11264 piece_hashes: &[irontide_core::Id32],
11265 file_root: irontide_core::Id32,
11266 piece_length: u64,
11267 file_length: u64,
11268 ) -> irontide_core::TorrentMetaV2 {
11269 use std::collections::BTreeMap;
11270
11271 let mut layer_bytes = Vec::with_capacity(piece_hashes.len() * 32);
11273 for h in piece_hashes {
11274 layer_bytes.extend_from_slice(&h.0);
11275 }
11276
11277 let mut piece_layers = BTreeMap::new();
11278 piece_layers.insert(file_root, layer_bytes);
11279
11280 let file_tree = irontide_core::FileTreeNode::Directory({
11281 let mut children = BTreeMap::new();
11282 children.insert(
11283 "test.dat".to_string(),
11284 irontide_core::FileTreeNode::File(irontide_core::V2FileAttr {
11285 length: file_length,
11286 pieces_root: Some(file_root),
11287 }),
11288 );
11289 children
11290 });
11291
11292 irontide_core::TorrentMetaV2 {
11293 info_hashes: irontide_core::InfoHashes::v2_only(irontide_core::Id32::ZERO),
11294 info_bytes: None,
11295 announce: None,
11296 announce_list: None,
11297 comment: None,
11298 created_by: None,
11299 creation_date: None,
11300 info: irontide_core::InfoDictV2 {
11301 name: "test".to_string(),
11302 piece_length,
11303 meta_version: 2,
11304 file_tree,
11305 ssl_cert: None,
11306 },
11307 piece_layers,
11308 ssl_cert: None,
11309 }
11310 }
11311
11312 #[test]
11313 fn test_serve_hashes_v2_piece_layer() {
11314 let hashes: Vec<irontide_core::Id32> = (0..4u8)
11317 .map(|i| {
11318 let mut h = [0u8; 32];
11319 h[0] = i;
11320 irontide_core::Id32(h)
11321 })
11322 .collect();
11323 let file_root = irontide_core::Id32([0xAA; 32]);
11324 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11325 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11326
11327 let request = irontide_core::HashRequest {
11328 file_root,
11329 base: 0, index: 0,
11331 count: 4,
11332 proof_layers: 0,
11333 };
11334
11335 let result = serve_hashes(
11336 Some(&meta),
11337 irontide_core::TorrentVersion::V2Only,
11338 Some(&lengths),
11339 &request,
11340 );
11341 let served = result.expect("should serve hashes");
11342 assert_eq!(served.len(), 4);
11343 for (i, h) in served.iter().enumerate() {
11344 assert_eq!(h.0[0], i as u8);
11345 }
11346 }
11347
11348 #[test]
11349 fn test_serve_hashes_rejects_v1_only() {
11350 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
11351 let file_root = irontide_core::Id32([0xAA; 32]);
11352 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
11353 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
11354
11355 let request = irontide_core::HashRequest {
11356 file_root,
11357 base: 0,
11358 index: 0,
11359 count: 1,
11360 proof_layers: 0,
11361 };
11362
11363 let result = serve_hashes(
11364 Some(&meta),
11365 irontide_core::TorrentVersion::V1Only,
11366 Some(&lengths),
11367 &request,
11368 );
11369 assert!(result.is_none(), "V1Only should reject hash requests");
11370 }
11371
11372 #[test]
11373 fn test_serve_hashes_rejects_unknown_root() {
11374 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
11375 let file_root = irontide_core::Id32([0xAA; 32]);
11376 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
11377 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
11378
11379 let unknown_root = irontide_core::Id32([0xFF; 32]);
11381 let request = irontide_core::HashRequest {
11382 file_root: unknown_root,
11383 base: 0,
11384 index: 0,
11385 count: 1,
11386 proof_layers: 0,
11387 };
11388
11389 let result = serve_hashes(
11390 Some(&meta),
11391 irontide_core::TorrentVersion::V2Only,
11392 Some(&lengths),
11393 &request,
11394 );
11395 assert!(result.is_none(), "unknown file_root should reject");
11396 }
11397
11398 #[test]
11399 fn test_serve_hashes_rejects_out_of_bounds() {
11400 let hashes: Vec<irontide_core::Id32> =
11402 (0..2u8).map(|i| irontide_core::Id32([i; 32])).collect();
11403 let file_root = irontide_core::Id32([0xAA; 32]);
11404 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 2);
11405 let lengths = Lengths::new(16384 * 2, 16384, DEFAULT_CHUNK_SIZE);
11406
11407 let request = irontide_core::HashRequest {
11409 file_root,
11410 base: 0,
11411 index: 5,
11412 count: 1,
11413 proof_layers: 0,
11414 };
11415
11416 let result = serve_hashes(
11417 Some(&meta),
11418 irontide_core::TorrentVersion::V2Only,
11419 Some(&lengths),
11420 &request,
11421 );
11422 assert!(result.is_none(), "out-of-bounds index should reject");
11423 }
11424
11425 #[test]
11426 fn test_serve_hashes_includes_proofs() {
11427 let hashes: Vec<irontide_core::Id32> =
11430 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
11431 let file_root = irontide_core::Id32([0xAA; 32]);
11432 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11433 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11434
11435 let request = irontide_core::HashRequest {
11437 file_root,
11438 base: 0,
11439 index: 0,
11440 count: 1,
11441 proof_layers: 1,
11442 };
11443
11444 let result = serve_hashes(
11445 Some(&meta),
11446 irontide_core::TorrentVersion::V2Only,
11447 Some(&lengths),
11448 &request,
11449 );
11450 let served = result.expect("should serve hashes with proofs");
11451 assert_eq!(served.len(), 2, "should have 1 data hash + 1 proof hash");
11453 assert_eq!(served[0], hashes[0]);
11455 assert_eq!(served[1], hashes[1]);
11457 }
11458
11459 #[test]
11460 fn test_serve_hashes_proof_with_batch() {
11461 let hashes: Vec<irontide_core::Id32> =
11476 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
11477 let file_root = irontide_core::Id32([0xAA; 32]);
11478 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
11479 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
11480
11481 let request = irontide_core::HashRequest {
11482 file_root,
11483 base: 0,
11484 index: 0,
11485 count: 2,
11486 proof_layers: 1,
11487 };
11488
11489 let result = serve_hashes(
11490 Some(&meta),
11491 irontide_core::TorrentVersion::V2Only,
11492 Some(&lengths),
11493 &request,
11494 );
11495 let served = result.expect("should serve hashes with batch proof");
11496 assert_eq!(served.len(), 3, "should have 2 data hashes + 1 uncle hash");
11498 assert_eq!(served[0], hashes[0]);
11500 assert_eq!(served[1], hashes[1]);
11501 let tree = irontide_core::MerkleTree::from_leaves(&hashes);
11504 let expected_uncle = tree.layer(1)[1]; assert_eq!(served[2], expected_uncle);
11506
11507 let sub_root = irontide_core::MerkleTree::root_from_hashes(&served[..2]);
11510 let uncle_hashes = &served[2..];
11511 let leaf_index = request.index as usize / 2; assert!(
11513 irontide_core::MerkleTree::verify_proof(
11514 tree.root(),
11515 sub_root,
11516 leaf_index,
11517 uncle_hashes
11518 ),
11519 "subtree proof should verify against tree root"
11520 );
11521 }
11522
11523 #[test]
11524 fn is_i2p_synthetic_addr_detects_240_range() {
11525 assert!(is_i2p_synthetic_addr(&"240.0.0.1:1".parse().unwrap()));
11526 assert!(is_i2p_synthetic_addr(
11527 &"255.255.255.255:65535".parse().unwrap()
11528 ));
11529 assert!(!is_i2p_synthetic_addr(&"192.168.1.1:6881".parse().unwrap()));
11530 assert!(!is_i2p_synthetic_addr(&"[::1]:6881".parse().unwrap()));
11531 }
11532
11533 #[test]
11534 fn v6_retry_delay_progression() {
11535 let expected_ms = [100, 200, 400, 800, 1600, 3200, 5000, 5000, 5000, 5000, 5000];
11537 for (count, &expected) in expected_ms.iter().enumerate() {
11538 let delay_ms = {
11539 let base_ms: u64 = 100;
11540 let max_ms: u64 = 5000;
11541 base_ms
11542 .saturating_mul(1u64.checked_shl(count as u32).unwrap_or(u64::MAX))
11543 .min(max_ms)
11544 };
11545 assert_eq!(
11546 delay_ms, expected,
11547 "count={count}: expected {expected}ms, got {delay_ms}ms"
11548 );
11549 }
11550 }
11551
11552 #[test]
11555 fn peer_backoff_exponential() {
11556 let expected_ms: Vec<u64> = vec![400, 800, 1600, 3200, 6400, 12800, 25600, 30000, 30000];
11559 for (i, &expected) in expected_ms.iter().enumerate() {
11560 let attempt = (i as u32) + 1; let delay_ms = 200u64.saturating_mul(1u64 << attempt.min(10)).min(30_000);
11562 assert_eq!(
11563 delay_ms, expected,
11564 "attempt={attempt}: expected {expected}ms, got {delay_ms}ms"
11565 );
11566 }
11567 }
11568
11569 #[test]
11570 fn peer_backoff_clears_on_data() {
11571 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
11574 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
11575
11576 assert!(!backoff.contains_key(&addr));
11578
11579 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
11581 let next = attempt.saturating_add(1);
11582 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
11583 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
11584 backoff.insert(addr, (earliest, next));
11585 assert_eq!(backoff.get(&addr).unwrap().1, 1);
11586
11587 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
11589 let next = attempt.saturating_add(1);
11590 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
11591 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
11592 backoff.insert(addr, (earliest, next));
11593 assert_eq!(backoff.get(&addr).unwrap().1, 2);
11594
11595 backoff.remove(&addr);
11597 assert!(!backoff.contains_key(&addr));
11598 }
11599
11600 #[test]
11601 fn backoff_prevents_hammering() {
11602 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
11604 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
11605
11606 let future = std::time::Instant::now() + Duration::from_secs(10);
11608 backoff.insert(addr, (future, 3));
11609
11610 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
11612 assert!(std::time::Instant::now() < next_attempt);
11613 }
11614
11615 let past = std::time::Instant::now() - Duration::from_secs(1);
11617 backoff.insert(addr, (past, 3));
11618 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
11619 assert!(std::time::Instant::now() >= next_attempt);
11620 }
11621 }
11622
11623 #[test]
11624 fn max_in_flight_formula_updated() {
11625 let formula = |connected: usize, num_pieces: u32| -> usize {
11627 let calculated = 512usize.max(connected.saturating_mul(4));
11628 calculated.min(num_pieces as usize / 2).max(512)
11629 };
11630
11631 assert_eq!(formula(10, 2000), 512);
11633
11634 assert_eq!(formula(200, 2000), 800);
11636
11637 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);
11649
11650 assert_eq!(formula(100, 0), 512);
11652 }
11653
11654 #[test]
11657 fn should_attempt_holepunch_reason_classification() {
11658 assert!(should_attempt_holepunch("connection refused"));
11660 assert!(should_attempt_holepunch("Connection refused"));
11661 assert!(should_attempt_holepunch("timed out"));
11662 assert!(should_attempt_holepunch("Connection reset by peer"));
11663 assert!(should_attempt_holepunch("connection reset by peer"));
11664 assert!(!should_attempt_holepunch(
11666 "holepunch TCP connect failed: Connection refused"
11667 ));
11668 assert!(!should_attempt_holepunch("peer banned"));
11670 assert!(!should_attempt_holepunch("protocol error"));
11671 assert!(!should_attempt_holepunch(""));
11672 }
11673
11674 #[test]
11675 fn holepunch_initiation_on_connect_failure() {
11676 assert!(should_attempt_holepunch("connection refused"));
11678 }
11679
11680 #[test]
11681 fn holepunch_cooldown_prevents_retry() {
11682 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
11683 let addr: SocketAddr = "127.0.0.1:6881".parse().expect("valid test addr");
11684 let now = Instant::now();
11685 cooldowns.insert(addr, now);
11686 assert!(cooldowns.contains_key(&addr));
11688 }
11689
11690 #[test]
11691 fn holepunch_cooldown_overflow_skips() {
11692 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
11693 let now = Instant::now();
11694 for i in 0..256u16 {
11695 let addr: SocketAddr = format!("10.0.{}.{}:6881", i / 256, i % 256)
11696 .parse()
11697 .expect("valid test addr");
11698 cooldowns.insert(addr, now);
11699 }
11700 assert_eq!(cooldowns.len(), HOLEPUNCH_MAX_TRACKED);
11701 }
11703
11704 #[test]
11705 fn holepunch_skipped_when_disabled() {
11706 assert!(should_attempt_holepunch("connection refused"));
11709 }
11711
11712 #[test]
11713 fn holepunch_not_triggered_on_ban() {
11714 assert!(!should_attempt_holepunch("peer banned"));
11715 assert!(!should_attempt_holepunch("banned for bad data"));
11716 }
11717
11718 fn make_multi_file_meta(files: &[(u64, &str)], piece_length: u64) -> TorrentMetaV1 {
11722 let total_length: u64 = files.iter().map(|(len, _)| *len).sum();
11723 let num_pieces = total_length.div_ceil(piece_length) as usize;
11724 let file_entries: Vec<irontide_core::FileEntry> = files
11725 .iter()
11726 .map(|(length, name)| irontide_core::FileEntry {
11727 length: *length,
11728 path: vec![name.to_string()],
11729 attr: None,
11730 mtime: None,
11731 symlink_path: None,
11732 })
11733 .collect();
11734 TorrentMetaV1 {
11735 info_hash: Id20([0u8; 20]),
11736 announce: None,
11737 announce_list: None,
11738 comment: None,
11739 created_by: None,
11740 creation_date: None,
11741 info: irontide_core::InfoDict {
11742 name: "test".to_string(),
11743 piece_length,
11744 pieces: vec![0u8; num_pieces * 20],
11745 length: None,
11746 files: Some(file_entries),
11747 private: None,
11748 source: None,
11749 ssl_cert: None,
11750 similar: Vec::new(),
11751 collections: Vec::new(),
11752 },
11753 url_list: Vec::new(),
11754 httpseeds: Vec::new(),
11755 info_bytes: None,
11756 ssl_cert: None,
11757 }
11758 }
11759
11760 #[test]
11761 fn cached_files_populated_on_registration() {
11762 let meta = make_multi_file_meta(&[(100, "a.txt"), (200, "b.txt"), (50, "c.txt")], 100);
11768 let lengths = Lengths::new(350, 100, 16384);
11769 let cached = build_cached_file_info(&meta, &lengths);
11770
11771 assert_eq!(cached.entries.len(), 3);
11772
11773 assert_eq!(cached.entries[0].index, 0);
11774 assert_eq!(cached.entries[0].length, 100);
11775 assert_eq!(cached.entries[0].first_piece, 0);
11776 assert_eq!(cached.entries[0].last_piece, 0);
11777
11778 assert_eq!(cached.entries[1].index, 1);
11779 assert_eq!(cached.entries[1].length, 200);
11780 assert_eq!(cached.entries[1].first_piece, 1);
11781 assert_eq!(cached.entries[1].last_piece, 2);
11782
11783 assert_eq!(cached.entries[2].index, 2);
11784 assert_eq!(cached.entries[2].length, 50);
11785 assert_eq!(cached.entries[2].first_piece, 3);
11786 assert_eq!(cached.entries[2].last_piece, 3);
11787 }
11788
11789 #[test]
11790 fn cached_files_single_file_torrent() {
11791 let meta = TorrentMetaV1 {
11794 info_hash: Id20([0u8; 20]),
11795 announce: None,
11796 announce_list: None,
11797 comment: None,
11798 created_by: None,
11799 creation_date: None,
11800 info: irontide_core::InfoDict {
11801 name: "single.bin".to_string(),
11802 piece_length: 100,
11803 pieces: vec![0u8; 5 * 20],
11804 length: Some(500),
11805 files: None,
11806 private: None,
11807 source: None,
11808 ssl_cert: None,
11809 similar: Vec::new(),
11810 collections: Vec::new(),
11811 },
11812 url_list: Vec::new(),
11813 httpseeds: Vec::new(),
11814 info_bytes: None,
11815 ssl_cert: None,
11816 };
11817 let lengths = Lengths::new(500, 100, 16384);
11818 let cached = build_cached_file_info(&meta, &lengths);
11819
11820 assert_eq!(cached.entries.len(), 1);
11821 assert_eq!(cached.entries[0].index, 0);
11822 assert_eq!(cached.entries[0].length, 500);
11823 assert_eq!(cached.entries[0].first_piece, 0);
11824 assert_eq!(cached.entries[0].last_piece, 4);
11825 }
11826
11827 use crate::piece_reservation::{AtomicPieceStates, PieceState, StealCandidates};
11834 use irontide_storage::Bitfield;
11835
11836 fn steal_populate_scan(states: &AtomicPieceStates, sc: &StealCandidates) -> u32 {
11840 let mut pushed = 0u32;
11841 let num = states.len();
11842 for piece in 0..num {
11843 let state = states.get(piece);
11844 if state == PieceState::Reserved {
11845 sc.push(piece);
11846 pushed = pushed.saturating_add(1);
11847 }
11848 }
11849 pushed
11850 }
11851
11852 fn all_wanted(n: u32) -> Bitfield {
11853 let mut bf = Bitfield::new(n);
11854 for i in 0..n {
11855 bf.set(i);
11856 }
11857 bf
11858 }
11859
11860 #[test]
11861 fn steal_populate_pushes_reserved_pieces() {
11862 let n = 10;
11863 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11864 let sc = StealCandidates::new();
11865
11866 assert!(states.try_reserve(2));
11868 assert!(states.try_reserve(5));
11869 assert!(states.try_reserve(7));
11870
11871 let pushed = steal_populate_scan(&states, &sc);
11872 assert_eq!(pushed, 3, "should push exactly the 3 reserved pieces");
11873
11874 let mut popped = Vec::new();
11876 while let Some(p) = sc.pop() {
11877 popped.push(p);
11878 }
11879 popped.sort_unstable();
11880 assert_eq!(popped, vec![2, 5, 7]);
11881 }
11882
11883 #[test]
11884 fn steal_populate_skips_non_reserved_states() {
11885 let n = 8;
11886 let mut have = Bitfield::new(n);
11887 have.set(0); let mut wanted = all_wanted(n);
11889 wanted.clear(1); let states = AtomicPieceStates::new(n, &have, &wanted);
11892 let sc = StealCandidates::new();
11893
11894 assert!(states.try_reserve(3));
11896
11897 let pushed = steal_populate_scan(&states, &sc);
11898 assert_eq!(pushed, 1, "only piece 3 (Reserved) should be pushed");
11899
11900 assert_eq!(sc.pop(), Some(3));
11901 assert_eq!(sc.pop(), None);
11902 }
11903
11904 #[test]
11905 fn steal_populate_deduplicates() {
11906 let n = 4;
11907 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11908 let sc = StealCandidates::new();
11909
11910 assert!(states.try_reserve(1));
11911 assert!(states.try_reserve(2));
11912
11913 let pushed1 = steal_populate_scan(&states, &sc);
11915 assert_eq!(pushed1, 2);
11916
11917 let pushed2 = steal_populate_scan(&states, &sc);
11920 assert_eq!(pushed2, 2, "scan still reports 2 reserved pieces");
11921
11922 let mut count = 0u32;
11923 while sc.pop().is_some() {
11924 count = count.saturating_add(1);
11925 }
11926 assert_eq!(count, 2, "dedup means only 2 entries despite 2 scans");
11927 }
11928
11929 #[test]
11930 fn steal_populate_skips_completed_pieces() {
11931 let n = 5;
11932 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11933 let sc = StealCandidates::new();
11934
11935 for i in 0..n {
11937 assert!(states.try_reserve(i));
11938 }
11939
11940 states.mark_complete(1);
11942 states.mark_complete(3);
11943
11944 let pushed = steal_populate_scan(&states, &sc);
11945 assert_eq!(pushed, 3, "3 pieces still Reserved (0, 2, 4)");
11946
11947 let mut popped = Vec::new();
11948 while let Some(p) = sc.pop() {
11949 popped.push(p);
11950 }
11951 popped.sort_unstable();
11952 assert_eq!(popped, vec![0, 2, 4]);
11953 }
11954
11955 #[test]
11956 fn steal_populate_empty_when_no_reserved() {
11957 let n = 6;
11958 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11959 let sc = StealCandidates::new();
11960
11961 let pushed = steal_populate_scan(&states, &sc);
11963 assert_eq!(pushed, 0);
11964 assert_eq!(sc.pop(), None);
11965 }
11966
11967 #[test]
11968 fn steal_populate_with_endgame_pieces() {
11969 let n = 4;
11971 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11972 let sc = StealCandidates::new();
11973
11974 assert!(states.try_reserve(0));
11975 assert!(states.try_reserve(1));
11976 states.transition_to_endgame(1);
11977
11978 let pushed = steal_populate_scan(&states, &sc);
11979 assert_eq!(
11980 pushed, 1,
11981 "only piece 0 (Reserved) should be pushed, not piece 1 (Endgame)"
11982 );
11983 assert_eq!(sc.pop(), Some(0));
11984 assert_eq!(sc.pop(), None);
11985 }
11986
11987 #[test]
11992 fn sync_piece_states_marks_unwanted_on_skip() {
11993 let n = 8;
11994 let mut wanted = all_wanted(n);
11995 wanted.clear(2);
11996 wanted.clear(3);
11997 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
11998 assert_eq!(states.get(2), PieceState::Available);
12001 assert_eq!(states.get(3), PieceState::Available);
12002
12003 for piece in 0..n {
12005 let w = wanted.get(piece);
12006 let current = states.get(piece);
12007 if !w && current == PieceState::Available {
12008 states.mark_unwanted(piece);
12009 } else if w && current == PieceState::Unwanted {
12010 states.mark_available(piece);
12011 }
12012 }
12013
12014 assert_eq!(states.get(0), PieceState::Available);
12015 assert_eq!(states.get(2), PieceState::Unwanted);
12016 assert_eq!(states.get(3), PieceState::Unwanted);
12017 assert_eq!(states.get(4), PieceState::Available);
12018 }
12019
12020 #[test]
12021 fn sync_piece_states_restores_available_on_unskip() {
12022 let n = 6;
12023 let mut initial_wanted = all_wanted(n);
12024 initial_wanted.clear(1);
12025 initial_wanted.clear(4);
12026 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &initial_wanted);
12027 assert_eq!(states.get(1), PieceState::Unwanted);
12028 assert_eq!(states.get(4), PieceState::Unwanted);
12029
12030 let new_wanted = all_wanted(n);
12032 for piece in 0..n {
12033 let w = new_wanted.get(piece);
12034 let current = states.get(piece);
12035 if !w && current == PieceState::Available {
12036 states.mark_unwanted(piece);
12037 } else if w && current == PieceState::Unwanted {
12038 states.mark_available(piece);
12039 }
12040 }
12041
12042 assert_eq!(states.get(1), PieceState::Available);
12043 assert_eq!(states.get(4), PieceState::Available);
12044 }
12045
12046 #[test]
12047 fn sync_piece_states_shared_piece_stays_available() {
12048 let n = 4;
12052 let mut wanted = all_wanted(n);
12053 wanted.clear(0); let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
12056
12057 for piece in 0..n {
12058 let w = wanted.get(piece);
12059 let current = states.get(piece);
12060 if !w && current == PieceState::Available {
12061 states.mark_unwanted(piece);
12062 } else if w && current == PieceState::Unwanted {
12063 states.mark_available(piece);
12064 }
12065 }
12066
12067 assert_eq!(states.get(0), PieceState::Unwanted);
12068 assert_eq!(
12069 states.get(1),
12070 PieceState::Available,
12071 "shared piece stays Available"
12072 );
12073 assert_eq!(states.get(2), PieceState::Available);
12074 assert_eq!(states.get(3), PieceState::Available);
12075 }
12076
12077 #[test]
12086 fn dht_requery_guard_scales_with_max_peers() {
12087 assert_eq!(128_usize.saturating_mul(4), 512);
12089
12090 assert_eq!(200_usize.saturating_mul(4), 800);
12092
12093 assert_eq!(50_usize.saturating_mul(4), 200);
12095
12096 assert_eq!(usize::MAX.saturating_mul(4), usize::MAX);
12098 }
12099
12100 fn make_test_info_bytes() -> (Vec<u8>, Id20) {
12104 use serde::Serialize;
12105
12106 #[derive(Serialize)]
12107 struct Info<'a> {
12108 length: u64,
12109 name: &'a str,
12110 #[serde(rename = "piece length")]
12111 piece_length: u64,
12112 #[serde(with = "serde_bytes")]
12113 pieces: &'a [u8],
12114 }
12115
12116 let data = vec![0xAB; 1024];
12117 let piece_hash = irontide_core::sha1(&data);
12118 let mut pieces = Vec::new();
12119 pieces.extend_from_slice(piece_hash.as_bytes());
12120
12121 let info = Info {
12122 length: 1024,
12123 name: "test",
12124 piece_length: 16384,
12125 pieces: &pieces,
12126 };
12127
12128 let info_bytes = irontide_bencode::to_bytes(&info).unwrap();
12129 let info_hash = irontide_core::sha1(&info_bytes);
12130 (info_bytes, info_hash)
12131 }
12132
12133 async fn create_magnet_handle(info_hash: Id20) -> TorrentHandle {
12135 let magnet = Magnet {
12136 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
12137 display_name: Some("test".into()),
12138 trackers: vec![],
12139 peers: vec![],
12140 selected_files: None,
12141 };
12142 let config = test_config();
12143 let (atx, amask) = test_alert_channel();
12144 let (dm, _dj) = test_disk_manager();
12145 TorrentHandle::from_magnet(
12146 magnet,
12147 dm,
12148 config,
12149 test_dht_rx(),
12150 test_dht_rx(),
12151 None,
12152 None,
12153 crate::slot_tuner::SlotTuner::disabled(4),
12154 atx,
12155 amask,
12156 None,
12157 None,
12158 test_ban_manager(),
12159 test_ip_filter(),
12160 Arc::new(Vec::new()),
12161 None,
12162 None,
12163 Arc::new(crate::transport::NetworkFactory::tokio()),
12164 None,
12165 Arc::new(crate::stats::SessionCounters::new()),
12166 )
12167 .await
12168 .unwrap()
12169 }
12170
12171 #[tokio::test]
12172 async fn pre_resolved_metadata_applies_when_fetching() {
12173 let (info_bytes, info_hash) = make_test_info_bytes();
12174 let handle = create_magnet_handle(info_hash).await;
12175
12176 let stats = handle.stats().await.unwrap();
12178 assert_eq!(stats.state, TorrentState::FetchingMetadata);
12179
12180 let peer_addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
12182 handle.send_pre_resolved_metadata(info_bytes, vec![peer_addr]);
12183
12184 tokio::time::sleep(Duration::from_millis(200)).await;
12186
12187 let stats = handle.stats().await.unwrap();
12189 assert_eq!(
12190 stats.state,
12191 TorrentState::Downloading,
12192 "should have transitioned to Downloading after pre-resolved metadata"
12193 );
12194 assert!(
12195 stats.pieces_total > 0,
12196 "should know piece count after metadata resolution"
12197 );
12198
12199 handle.shutdown().await.unwrap();
12200 }
12201
12202 #[tokio::test]
12203 async fn pre_resolved_metadata_ignored_after_resolution() {
12204 let data = vec![0xAB; 32768];
12206 let meta = make_test_torrent(&data, 16384);
12207 let info_hash = meta.info_hash;
12208 let storage = make_storage(&data, 16384);
12209 let config = test_config();
12210
12211 let (atx, amask) = test_alert_channel();
12212 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
12213 let handle = TorrentHandle::from_torrent(
12214 meta,
12215 irontide_core::TorrentVersion::V1Only,
12216 None,
12217 dh,
12218 dm,
12219 config,
12220 test_dht_rx(),
12221 test_dht_rx(),
12222 None,
12223 None,
12224 crate::slot_tuner::SlotTuner::disabled(4),
12225 atx,
12226 amask,
12227 None,
12228 None,
12229 test_ban_manager(),
12230 test_ip_filter(),
12231 Arc::new(Vec::new()),
12232 None,
12233 None,
12234 Arc::new(crate::transport::NetworkFactory::tokio()),
12235 None,
12236 Arc::new(crate::stats::SessionCounters::new()),
12237 )
12238 .await
12239 .unwrap();
12240
12241 let stats_before = handle.stats().await.unwrap();
12242 assert_eq!(stats_before.state, TorrentState::Downloading);
12243
12244 let (info_bytes, _) = make_test_info_bytes();
12247 handle.send_pre_resolved_metadata(info_bytes, vec![]);
12248
12249 tokio::time::sleep(Duration::from_millis(100)).await;
12251
12252 let stats_after = handle.stats().await.unwrap();
12254 assert_eq!(stats_after.state, TorrentState::Downloading);
12255 assert_eq!(stats_after.pieces_total, stats_before.pieces_total);
12256
12257 handle.shutdown().await.unwrap();
12258 }
12259
12260 #[tokio::test]
12261 async fn pre_resolved_metadata_with_invalid_hash_stays_fetching() {
12262 let (info_bytes, _correct_hash) = make_test_info_bytes();
12266
12267 let wrong_hash = Id20::from_hex("0000000000000000000000000000000000000001").unwrap();
12269 let handle = create_magnet_handle(wrong_hash).await;
12270
12271 let stats = handle.stats().await.unwrap();
12272 assert_eq!(stats.state, TorrentState::FetchingMetadata);
12273
12274 handle.send_pre_resolved_metadata(info_bytes, vec![]);
12276
12277 tokio::time::sleep(Duration::from_millis(200)).await;
12278
12279 let stats = handle.stats().await.unwrap();
12281 assert_eq!(
12282 stats.state,
12283 TorrentState::FetchingMetadata,
12284 "should stay in FetchingMetadata when info_hash doesn't match"
12285 );
12286
12287 handle.shutdown().await.unwrap();
12288 }
12289
12290 #[test]
12291 fn initial_queue_depth_is_128() {
12292 use crate::peer_shared::INITIAL_QUEUE_DEPTH;
12293 assert_eq!(INITIAL_QUEUE_DEPTH, 128);
12294 }
12295
12296 #[tokio::test]
12309 #[allow(
12310 clippy::large_stack_arrays,
12311 reason = "test data buffer passed directly to make_storage"
12312 )]
12313 async fn m159_seed_mode_suppresses_new_requests_on_wire() {
12314 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let info_hash = meta.info_hash;
12317 let storage = make_storage(&[0u8; 32768], 16384);
12319
12320 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
12321 let listen_addr = listener.local_addr().unwrap();
12322 let config = TorrentConfig {
12323 listen_port: listen_addr.port(),
12324 ..test_config()
12325 };
12326 drop(listener);
12327
12328 let (atx, amask) = test_alert_channel();
12329 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
12330 let handle = TorrentHandle::from_torrent(
12331 meta,
12332 irontide_core::TorrentVersion::V1Only,
12333 None,
12334 dh,
12335 dm,
12336 config,
12337 test_dht_rx(),
12338 test_dht_rx(),
12339 None,
12340 None,
12341 crate::slot_tuner::SlotTuner::disabled(4),
12342 atx,
12343 amask,
12344 None,
12345 None,
12346 test_ban_manager(),
12347 test_ip_filter(),
12348 Arc::new(Vec::new()),
12349 None,
12350 None,
12351 Arc::new(crate::transport::NetworkFactory::tokio()),
12352 None,
12353 Arc::new(crate::stats::SessionCounters::new()),
12354 )
12355 .await
12356 .unwrap();
12357
12358 tokio::time::sleep(Duration::from_millis(50)).await;
12359
12360 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
12362 let (reader, writer) = tokio::io::split(stream);
12363 let mut writer = writer;
12364 let mut reader = reader;
12365
12366 let hs = Handshake::new(
12367 info_hash,
12368 Id20::from_hex("dddddddddddddddddddddddddddddddddddddddd").unwrap(),
12369 );
12370 writer.write_all(&hs.to_bytes()).await.unwrap();
12371 writer.flush().await.unwrap();
12372 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
12373 reader.read_exact(&mut hs_buf).await.unwrap();
12374
12375 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
12376 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
12377
12378 let _actor_ext_hs = framed_read.next().await;
12380 let ext_hs = ExtHandshake::new();
12381 let ext_payload = ext_hs.to_bytes().unwrap();
12382 framed_write
12383 .send(Message::Extended {
12384 ext_id: 0,
12385 payload: ext_payload,
12386 })
12387 .await
12388 .unwrap();
12389
12390 let mut bf = Bitfield::new(2);
12392 bf.set(0);
12393 bf.set(1);
12394 framed_write
12395 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
12396 .await
12397 .unwrap();
12398 framed_write.send(Message::Unchoke).await.unwrap();
12399
12400 let mut initial_request_seen = false;
12404 let wait_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12405 loop {
12406 let remaining = wait_deadline.saturating_duration_since(tokio::time::Instant::now());
12407 if remaining.is_zero() {
12408 break;
12409 }
12410 match tokio::time::timeout(remaining, framed_read.next()).await {
12411 Ok(Some(Ok(Message::Request { .. }))) => {
12412 initial_request_seen = true;
12413 break;
12414 }
12415 Ok(Some(Ok(_))) => {}
12416 _ => break,
12417 }
12418 }
12419 assert!(
12420 initial_request_seen,
12421 "actor should have sent a Request before seed mode toggle"
12422 );
12423
12424 handle.set_seed_mode(true).await.unwrap();
12427
12428 let grace_deadline = tokio::time::Instant::now() + Duration::from_millis(200);
12435 let mut cancel_seen = false;
12436 let mut grace_requests = 0u32;
12437 loop {
12438 let remaining = grace_deadline.saturating_duration_since(tokio::time::Instant::now());
12439 if remaining.is_zero() {
12440 break;
12441 }
12442 match tokio::time::timeout(remaining, framed_read.next()).await {
12443 Ok(Some(Ok(Message::Request { .. }))) => {
12444 grace_requests += 1;
12445 }
12446 Ok(Some(Ok(Message::Cancel { .. }))) => {
12447 cancel_seen = true;
12448 }
12449 Ok(Some(Ok(_))) => {}
12450 Ok(None | Some(Err(_))) | Err(_) => break,
12451 }
12452 }
12453 let _ = (cancel_seen, grace_requests);
12454
12455 let steady_deadline = tokio::time::Instant::now() + Duration::from_millis(500);
12458 let mut steady_requests = 0u32;
12459 loop {
12460 let remaining = steady_deadline.saturating_duration_since(tokio::time::Instant::now());
12461 if remaining.is_zero() {
12462 break;
12463 }
12464 match tokio::time::timeout(remaining, framed_read.next()).await {
12465 Ok(Some(Ok(Message::Request { .. }))) => {
12466 steady_requests += 1;
12467 }
12468 Ok(Some(Ok(_))) => {}
12469 Ok(None | Some(Err(_))) | Err(_) => break,
12470 }
12471 }
12472
12473 assert_eq!(
12474 steady_requests, 0,
12475 "after the Stop propagation grace window, no new Request messages \
12476 must appear during steady-state while user_seed_mode is active"
12477 );
12478
12479 let stats = handle.stats().await.unwrap();
12481 assert!(
12482 stats.user_seed_mode,
12483 "stats.user_seed_mode should be true after set_seed_mode(true)"
12484 );
12485
12486 handle.shutdown().await.unwrap();
12487 }
12488
12489 #[tokio::test]
12517 async fn m159_seed_mode_uploads_continue_on_wire() {
12518 const FILL_BYTE: u8 = 0x5A;
12519 const PIECE_LENGTH: u64 = 16384;
12520 const TOTAL_LEN: usize = 32768; let data = vec![FILL_BYTE; TOTAL_LEN];
12523 let meta = make_test_torrent(&data, PIECE_LENGTH);
12524 let info_hash = meta.info_hash;
12525 let storage = make_seeded_storage(&data, PIECE_LENGTH);
12527
12528 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
12529 let listen_addr = listener.local_addr().unwrap();
12530 let config = TorrentConfig {
12531 listen_port: listen_addr.port(),
12532 ..test_config()
12533 };
12534 drop(listener);
12535
12536 let (atx, amask) = test_alert_channel();
12537 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
12538 let handle = TorrentHandle::from_torrent(
12539 meta,
12540 irontide_core::TorrentVersion::V1Only,
12541 None,
12542 dh,
12543 dm,
12544 config,
12545 test_dht_rx(),
12546 test_dht_rx(),
12547 None,
12548 None,
12549 crate::slot_tuner::SlotTuner::disabled(4),
12550 atx,
12551 amask,
12552 None,
12553 None,
12554 test_ban_manager(),
12555 test_ip_filter(),
12556 Arc::new(Vec::new()),
12557 None,
12558 None,
12559 Arc::new(crate::transport::NetworkFactory::tokio()),
12560 None,
12561 Arc::new(crate::stats::SessionCounters::new()),
12562 )
12563 .await
12564 .unwrap();
12565
12566 let seeding_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12569 loop {
12570 tokio::time::sleep(Duration::from_millis(50)).await;
12571 let stats = handle.stats().await.unwrap();
12572 if stats.state == TorrentState::Seeding && stats.pieces_have == 2 {
12573 break;
12574 }
12575 if tokio::time::Instant::now() > seeding_deadline {
12576 let stats = handle.stats().await.unwrap();
12577 panic!(
12578 "actor did not reach Seeding state within 3s: state={:?}, have={}/{}",
12579 stats.state, stats.pieces_have, stats.pieces_total
12580 );
12581 }
12582 }
12583
12584 handle.set_seed_mode(true).await.unwrap();
12587 let stats = handle.stats().await.unwrap();
12588 assert!(
12589 stats.user_seed_mode,
12590 "stats.user_seed_mode should be true after set_seed_mode(true)"
12591 );
12592
12593 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
12595 let (reader, writer) = tokio::io::split(stream);
12596 let mut writer = writer;
12597 let mut reader = reader;
12598
12599 let hs = Handshake::new(
12600 info_hash,
12601 Id20::from_hex("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(),
12602 );
12603 writer.write_all(&hs.to_bytes()).await.unwrap();
12604 writer.flush().await.unwrap();
12605 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
12606 reader.read_exact(&mut hs_buf).await.unwrap();
12607
12608 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
12609 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
12610
12611 let _actor_ext_hs = framed_read.next().await;
12613 let ext_hs = ExtHandshake::new();
12614 let ext_payload = ext_hs.to_bytes().unwrap();
12615 framed_write
12616 .send(Message::Extended {
12617 ext_id: 0,
12618 payload: ext_payload,
12619 })
12620 .await
12621 .unwrap();
12622
12623 let bf = Bitfield::new(2);
12625 framed_write
12626 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
12627 .await
12628 .unwrap();
12629 framed_write.send(Message::Interested).await.unwrap();
12630
12631 let unchoke_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
12635 let mut saw_unchoke = false;
12636 loop {
12637 let remaining = unchoke_deadline.saturating_duration_since(tokio::time::Instant::now());
12638 if remaining.is_zero() {
12639 break;
12640 }
12641 match tokio::time::timeout(remaining, framed_read.next()).await {
12642 Ok(Some(Ok(Message::Unchoke))) => {
12643 saw_unchoke = true;
12644 break;
12645 }
12646 Ok(Some(Ok(_))) => {}
12647 Ok(None | Some(Err(_))) => break,
12648 Err(_elapsed) => break,
12649 }
12650 }
12651 assert!(
12652 saw_unchoke,
12653 "actor should have unchoked the leecher while user_seed_mode is active"
12654 );
12655
12656 framed_write
12659 .send(Message::Request {
12660 index: 0,
12661 begin: 0,
12662 length: PIECE_LENGTH as u32,
12663 })
12664 .await
12665 .unwrap();
12666
12667 let piece_deadline = tokio::time::Instant::now() + Duration::from_secs(2);
12672 let mut got_piece = false;
12673 loop {
12674 let remaining = piece_deadline.saturating_duration_since(tokio::time::Instant::now());
12675 if remaining.is_zero() {
12676 break;
12677 }
12678 match tokio::time::timeout(remaining, framed_read.next()).await {
12679 Ok(Some(Ok(Message::Piece {
12680 index,
12681 begin,
12682 data_0,
12683 data_1,
12684 }))) => {
12685 assert_eq!(index, 0, "Piece index should match request");
12686 assert_eq!(begin, 0, "Piece begin should match request");
12687 let mut payload: Vec<u8> =
12688 Vec::with_capacity(data_0.len().saturating_add(data_1.len()));
12689 payload.extend_from_slice(&data_0);
12690 payload.extend_from_slice(&data_1);
12691 assert_eq!(
12692 payload.len(),
12693 PIECE_LENGTH as usize,
12694 "Piece payload length should match requested length"
12695 );
12696 assert!(
12697 payload.iter().all(|&b| b == FILL_BYTE),
12698 "Piece payload should contain the pre-seeded fill byte"
12699 );
12700 got_piece = true;
12701 break;
12702 }
12703 Ok(Some(Ok(_))) => {}
12704 Ok(None | Some(Err(_))) => break,
12705 Err(_elapsed) => break,
12706 }
12707 }
12708 assert!(
12709 got_piece,
12710 "actor should have served a Piece in response to Request while user_seed_mode is active"
12711 );
12712
12713 let stats = handle.stats().await.unwrap();
12716 assert!(
12717 stats.user_seed_mode,
12718 "stats.user_seed_mode should remain true after serving an upload"
12719 );
12720 assert!(
12721 stats.uploaded >= u64::from(PIECE_LENGTH as u32),
12722 "stats.uploaded should reflect the served block, got {}",
12723 stats.uploaded
12724 );
12725
12726 handle.shutdown().await.unwrap();
12727 }
12728
12729 #[tokio::test]
12732 async fn info_field_populated_for_torrent() {
12733 let data = vec![0xAB; 32768];
12734 let meta = make_test_torrent(&data, 16384);
12735 let storage = make_storage(&data, 16384);
12736 let config = test_config();
12737
12738 let (atx, amask) = test_alert_channel();
12739 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
12740 let handle = TorrentHandle::from_torrent(
12741 meta,
12742 irontide_core::TorrentVersion::V1Only,
12743 None,
12744 dh,
12745 dm,
12746 config,
12747 test_dht_rx(),
12748 test_dht_rx(),
12749 None,
12750 None,
12751 crate::slot_tuner::SlotTuner::disabled(4),
12752 atx,
12753 amask,
12754 None,
12755 None,
12756 test_ban_manager(),
12757 test_ip_filter(),
12758 Arc::new(Vec::new()),
12759 None,
12760 None,
12761 Arc::new(crate::transport::NetworkFactory::tokio()),
12762 None,
12763 Arc::new(crate::stats::SessionCounters::new()),
12764 )
12765 .await
12766 .unwrap();
12767
12768 tokio::time::sleep(Duration::from_millis(50)).await;
12769
12770 let rd = handle.save_resume_data().await.unwrap();
12771
12772 assert!(rd.info.is_some(), "rd.info should be Some for .torrent");
12774
12775 let info_bytes = rd.info.as_ref().unwrap();
12777 let info: irontide_core::InfoDict =
12778 irontide_bencode::from_bytes(info_bytes).expect("info bytes should deserialize");
12779 assert_eq!(info.name, "test");
12780 assert_eq!(info.piece_length, 16384);
12781
12782 handle.shutdown().await.unwrap();
12783 }
12784
12785 #[tokio::test]
12786 async fn info_hash2_none_for_v1_only() {
12787 let data = vec![0xCD; 16384];
12788 let meta = make_test_torrent(&data, 16384);
12789 let storage = make_storage(&data, 16384);
12790 let config = test_config();
12791
12792 let (atx, amask) = test_alert_channel();
12793 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
12794 let handle = TorrentHandle::from_torrent(
12795 meta,
12796 irontide_core::TorrentVersion::V1Only,
12797 None,
12798 dh,
12799 dm,
12800 config,
12801 test_dht_rx(),
12802 test_dht_rx(),
12803 None,
12804 None,
12805 crate::slot_tuner::SlotTuner::disabled(4),
12806 atx,
12807 amask,
12808 None,
12809 None,
12810 test_ban_manager(),
12811 test_ip_filter(),
12812 Arc::new(Vec::new()),
12813 None,
12814 None,
12815 Arc::new(crate::transport::NetworkFactory::tokio()),
12816 None,
12817 Arc::new(crate::stats::SessionCounters::new()),
12818 )
12819 .await
12820 .unwrap();
12821
12822 tokio::time::sleep(Duration::from_millis(50)).await;
12823
12824 let rd = handle.save_resume_data().await.unwrap();
12825
12826 assert!(
12828 rd.info_hash2.is_none(),
12829 "v1-only torrent should have info_hash2 = None"
12830 );
12831
12832 assert!(
12834 rd.added_time > 0,
12835 "added_time should be a positive POSIX timestamp"
12836 );
12837
12838 handle.shutdown().await.unwrap();
12839 }
12840
12841 #[tokio::test]
12842 async fn info_none_for_unresolved_magnet() {
12843 let magnet = Magnet {
12844 info_hashes: irontide_core::InfoHashes::v1_only(
12845 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
12846 ),
12847 display_name: Some("magnet-test".into()),
12848 trackers: vec![],
12849 peers: vec![],
12850 selected_files: None,
12851 };
12852 let config = test_config();
12853
12854 let (atx, amask) = test_alert_channel();
12855 let (dm, _dj) = test_disk_manager();
12856 let handle = TorrentHandle::from_magnet(
12857 magnet,
12858 dm,
12859 config,
12860 test_dht_rx(),
12861 test_dht_rx(),
12862 None,
12863 None,
12864 crate::slot_tuner::SlotTuner::disabled(4),
12865 atx,
12866 amask,
12867 None,
12868 None,
12869 test_ban_manager(),
12870 test_ip_filter(),
12871 Arc::new(Vec::new()),
12872 None,
12873 None,
12874 Arc::new(crate::transport::NetworkFactory::tokio()),
12875 None,
12876 Arc::new(crate::stats::SessionCounters::new()),
12877 )
12878 .await
12879 .unwrap();
12880
12881 tokio::time::sleep(Duration::from_millis(50)).await;
12882
12883 let rd = handle.save_resume_data().await.unwrap();
12884
12885 assert!(
12887 rd.info.is_none(),
12888 "unresolved magnet should have info = None"
12889 );
12890
12891 assert!(
12893 rd.added_time > 0,
12894 "added_time should be set for magnet links"
12895 );
12896
12897 handle.shutdown().await.unwrap();
12898 }
12899
12900 #[tokio::test]
12903 async fn torrent_command_get_meta_returns_none_before_metadata() {
12904 let info_hash =
12906 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").expect("valid hex");
12907 let handle = create_magnet_handle(info_hash).await;
12908
12909 let (tx, rx) = oneshot::channel();
12910 handle
12911 .cmd_tx
12912 .send(TorrentCommand::GetMeta { reply: tx })
12913 .await
12914 .expect("cmd_tx send");
12915 let result = rx.await.expect("GetMeta reply");
12916 assert!(
12917 result.is_none(),
12918 "pre-metadata magnet must return None from GetMeta"
12919 );
12920
12921 handle.shutdown().await.unwrap();
12922 }
12923
12924 #[tokio::test]
12925 async fn torrent_command_get_meta_returns_some_after_metadata() {
12926 let (info_bytes, info_hash) = make_test_info_bytes();
12929 let handle = create_magnet_handle(info_hash).await;
12930
12931 handle.send_pre_resolved_metadata(info_bytes, vec![]);
12932
12933 let mut result = None;
12937 for _ in 0..100 {
12938 tokio::time::sleep(Duration::from_millis(20)).await;
12939 let (tx, rx) = oneshot::channel();
12940 handle
12941 .cmd_tx
12942 .send(TorrentCommand::GetMeta { reply: tx })
12943 .await
12944 .expect("cmd_tx send");
12945 let r = rx.await.expect("GetMeta reply");
12946 if r.is_some() {
12947 result = r;
12948 break;
12949 }
12950 }
12951 let meta = result.expect("GetMeta must return Some after PreResolvedMetadata");
12952 assert_eq!(meta.info_hash, info_hash);
12953
12954 handle.shutdown().await.unwrap();
12955 }
12956
12957 #[tokio::test]
12960 async fn web_seed_progress_idle_to_active_on_first_success() {
12961 let mut actor = TorrentActor::for_throttle_test(8, 0);
12962 actor.handle_web_seed_progress("http://seed.example/file", 1024, 1_000_000, None);
12963 let stats = actor
12964 .web_seed_stats
12965 .get("http://seed.example/file")
12966 .expect("stats inserted");
12967 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12968 assert_eq!(stats.downloaded_bytes, 1024);
12969 assert_eq!(stats.last_rate_bps, 1_000_000);
12970 assert_eq!(stats.consecutive_failures, 0);
12971 assert!(stats.last_attempt_unix_secs > 0);
12972 assert!(actor.need_save_resume);
12973 }
12974
12975 #[tokio::test]
12976 async fn web_seed_progress_active_to_errored_then_recovery_persists_last_error() {
12977 let mut actor = TorrentActor::for_throttle_test(8, 0);
12978 let url = "http://seed.example/file".to_string();
12979
12980 actor.handle_web_seed_progress(&url, 1024, 100, None);
12982 assert_eq!(
12983 actor.web_seed_stats[&url].state,
12984 irontide_core::WebSeedState::Active
12985 );
12986
12987 actor.handle_web_seed_progress(&url, 1024, 0, Some("503".into()));
12989 let stats = &actor.web_seed_stats[&url];
12990 assert_eq!(stats.state, irontide_core::WebSeedState::Errored);
12991 assert_eq!(stats.last_error.as_deref(), Some("503"));
12992 assert_eq!(stats.consecutive_failures, 1);
12993
12994 actor.handle_web_seed_progress(&url, 2048, 200, None);
12996 let stats = &actor.web_seed_stats[&url];
12997 assert_eq!(stats.state, irontide_core::WebSeedState::Active);
12998 assert_eq!(
12999 stats.last_error.as_deref(),
13000 Some("503"),
13001 "last_error must persist through recovery (D-eng-8)"
13002 );
13003 assert_eq!(
13004 stats.consecutive_failures, 0,
13005 "consecutive_failures resets on success"
13006 );
13007 }
13008
13009 #[tokio::test]
13010 async fn web_seed_progress_consecutive_failures_monotonic_within_run() {
13011 let mut actor = TorrentActor::for_throttle_test(8, 0);
13012 let url = "http://seed.example/file".to_string();
13013
13014 actor.handle_web_seed_progress(&url, 0, 0, Some("e1".into()));
13015 actor.handle_web_seed_progress(&url, 0, 0, Some("e2".into()));
13016 actor.handle_web_seed_progress(&url, 0, 0, Some("e3".into()));
13017 let stats = &actor.web_seed_stats[&url];
13018 assert_eq!(stats.consecutive_failures, 3);
13019 assert_eq!(
13020 stats.last_error.as_deref(),
13021 Some("e3"),
13022 "last_error reflects most recent message"
13023 );
13024
13025 actor.handle_web_seed_progress(&url, 1024, 100, None);
13026 assert_eq!(
13027 actor.web_seed_stats[&url].consecutive_failures, 0,
13028 "success resets consecutive_failures"
13029 );
13030 }
13031
13032 fn install_peer_states(actor: &mut TorrentActor) {
13037 let (queue_tx, _queue_rx) = mpsc::unbounded_channel();
13038 actor.peer_states = Some(std::sync::Arc::new(crate::peer_states::PeerStates::new(
13039 queue_tx,
13040 )));
13041 }
13042
13043 fn addr(octet: u8, port: u16) -> std::net::SocketAddr {
13044 std::net::SocketAddr::new(
13045 std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 0, 2, octet)),
13046 port,
13047 )
13048 }
13049
13050 #[tokio::test]
13051 async fn pex_count_dedups_same_peer_in_two_messages() {
13052 let mut actor = TorrentActor::for_throttle_test(8, 0);
13053 install_peer_states(&mut actor);
13054
13055 actor.handle_add_peers(
13057 vec![addr(1, 6881), addr(2, 6881)],
13058 crate::peer_state::PeerSource::Pex,
13059 );
13060 actor.handle_add_peers(
13062 vec![addr(1, 6881), addr(3, 6881)],
13063 crate::peer_state::PeerSource::Pex,
13064 );
13065 assert_eq!(
13066 actor.pex_peer_count, 3,
13067 "3 unique peers across 2 PEX messages, A counted once"
13068 );
13069 assert_eq!(actor.lsd_peer_count, 0, "LSD untouched");
13070 }
13071
13072 #[tokio::test]
13073 async fn lsd_count_aggregates_across_multicasts() {
13074 let mut actor = TorrentActor::for_throttle_test(8, 0);
13075 install_peer_states(&mut actor);
13076
13077 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd);
13078 actor.handle_add_peers(
13079 vec![addr(2, 6881), addr(3, 6881)],
13080 crate::peer_state::PeerSource::Lsd,
13081 );
13082 actor.handle_add_peers(
13083 vec![addr(1, 6881)], crate::peer_state::PeerSource::Lsd,
13085 );
13086 assert_eq!(actor.lsd_peer_count, 3);
13087 }
13088
13089 #[tokio::test]
13090 async fn other_sources_do_not_bump_pex_or_lsd() {
13091 let mut actor = TorrentActor::for_throttle_test(8, 0);
13092 install_peer_states(&mut actor);
13093
13094 actor.handle_add_peers(
13095 vec![addr(1, 6881), addr(2, 6881)],
13096 crate::peer_state::PeerSource::Tracker,
13097 );
13098 actor.handle_add_peers(vec![addr(3, 6881)], crate::peer_state::PeerSource::Dht);
13099 actor.handle_add_peers(vec![addr(4, 6881)], crate::peer_state::PeerSource::Incoming);
13100 assert_eq!(actor.pex_peer_count, 0);
13101 assert_eq!(actor.lsd_peer_count, 0);
13102 }
13103
13104 #[tokio::test]
13105 async fn dedup_runs_against_global_seen_set() {
13106 let mut actor = TorrentActor::for_throttle_test(8, 0);
13112 install_peer_states(&mut actor);
13113
13114 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Tracker);
13115 actor.handle_add_peers(vec![addr(1, 6881)], crate::peer_state::PeerSource::Pex);
13116 assert_eq!(
13117 actor.pex_peer_count, 0,
13118 "peer already seen via tracker — PEX shouldn't re-count"
13119 );
13120 }
13121
13122 #[tokio::test]
13123 async fn web_seed_progress_dirties_resume_flag() {
13124 let mut actor = TorrentActor::for_throttle_test(8, 0);
13125 actor.need_save_resume = false;
13126 actor.handle_web_seed_progress("http://x/file", 100, 50, None);
13127 assert!(
13128 actor.need_save_resume,
13129 "every progress event should mark fast-resume dirty"
13130 );
13131 }
13132
13133 #[tokio::test]
13134 async fn paused_torrent_rejects_outbound_peer_connect() {
13135 let mut actor = TorrentActor::for_throttle_test(8, 0);
13136 install_peer_states(&mut actor);
13137 actor.state = TorrentState::Paused;
13138
13139 let sem = Arc::new(tokio::sync::Semaphore::new(1));
13140 let permit = sem.clone().acquire_owned().await.unwrap();
13141 let connect = crate::peer_adder::ConnectPeer {
13142 addr: addr(1, 6881),
13143 source: crate::peer_state::PeerSource::Dht,
13144 permit,
13145 };
13146 actor.handle_adder_connect(connect);
13147 assert!(
13148 actor.peers.is_empty(),
13149 "paused torrent must not accept outbound peer connections"
13150 );
13151 assert_eq!(
13152 sem.available_permits(),
13153 1,
13154 "semaphore permit must be released on rejection"
13155 );
13156 }
13157
13158 #[tokio::test]
13159 async fn resume_from_queued_restores_fetching_metadata_for_magnets() {
13160 let mut actor = TorrentActor::for_throttle_test(0, 0);
13161 actor.state = TorrentState::Queued;
13162 assert!(
13163 actor.chunk_tracker.is_none(),
13164 "magnet torrent has no chunk tracker before metadata"
13165 );
13166 assert_eq!(actor.num_pieces, 0);
13167
13168 actor.handle_resume().await;
13169 assert_eq!(
13170 actor.state,
13171 TorrentState::FetchingMetadata,
13172 "magnet torrent must resume to FetchingMetadata, not Downloading"
13173 );
13174 }
13175
13176 #[tokio::test]
13177 async fn resume_from_queued_restores_downloading_when_metadata_known() {
13178 let mut actor = TorrentActor::for_throttle_test(8, 0);
13179 actor.state = TorrentState::Queued;
13180
13181 actor.handle_resume().await;
13182 assert_eq!(
13183 actor.state,
13184 TorrentState::Downloading,
13185 "torrent with known pieces must resume to Downloading"
13186 );
13187 }
13188
13189 #[tokio::test]
13190 async fn queued_torrent_rejects_outbound_peer_connect() {
13191 let mut actor = TorrentActor::for_throttle_test(8, 0);
13192 install_peer_states(&mut actor);
13193 actor.state = TorrentState::Queued;
13194
13195 let sem = Arc::new(tokio::sync::Semaphore::new(1));
13196 let permit = sem.clone().acquire_owned().await.unwrap();
13197 let connect = crate::peer_adder::ConnectPeer {
13198 addr: addr(1, 6881),
13199 source: crate::peer_state::PeerSource::Dht,
13200 permit,
13201 };
13202 actor.handle_adder_connect(connect);
13203 assert!(
13204 actor.peers.is_empty(),
13205 "queued torrent must not accept outbound peer connections"
13206 );
13207 assert_eq!(
13208 sem.available_permits(),
13209 1,
13210 "semaphore permit must be released on rejection"
13211 );
13212 }
13213
13214 fn inject_peer_for_flush(
13218 actor: &mut TorrentActor,
13219 peer_addr: std::net::SocketAddr,
13220 unchoke_started: Option<std::time::Instant>,
13221 prior_total: std::time::Duration,
13222 ) {
13223 let (cmd_tx, _cmd_rx) = mpsc::channel(8);
13224 let mut peer = crate::peer_state::PeerState::new(
13225 peer_addr,
13226 actor.num_pieces,
13227 cmd_tx,
13228 crate::peer_state::PeerSource::Tracker,
13229 Arc::new(AtomicU32::new(0)),
13230 Arc::new(AtomicU32::new(128)),
13231 Arc::new(tokio::sync::Notify::new()),
13232 );
13233 peer.am_unchoke_started_at = unchoke_started;
13234 peer.unchoke_duration_total = prior_total;
13235 actor.peers.insert(peer_addr, peer);
13236 }
13237
13238 #[tokio::test]
13239 async fn disconnect_while_unchoked_flushes_delta_into_torrent_map() {
13240 let mut actor = TorrentActor::for_throttle_test(8, 0);
13241 let p = addr(1, 6881);
13242
13243 inject_peer_for_flush(
13246 &mut actor,
13247 p,
13248 Some(std::time::Instant::now() - std::time::Duration::from_millis(50)),
13249 std::time::Duration::from_millis(100),
13250 );
13251
13252 actor.disconnect_peer(p, "test");
13253
13254 let total = actor
13255 .unchoke_durations
13256 .get(&p)
13257 .copied()
13258 .expect("disconnect must flush a non-zero delta into the torrent map");
13259 assert!(
13260 total >= std::time::Duration::from_millis(140),
13261 "expected ≥140 ms (100 prior + ~50 in-flight), got {total:?}"
13262 );
13263 }
13264
13265 #[tokio::test]
13266 async fn disconnect_then_reconnect_preserves_history() {
13267 let mut actor = TorrentActor::for_throttle_test(8, 0);
13268 let p = addr(2, 6881);
13269
13270 inject_peer_for_flush(&mut actor, p, None, std::time::Duration::from_millis(80));
13272 actor.disconnect_peer(p, "test");
13273 let after_first = *actor
13274 .unchoke_durations
13275 .get(&p)
13276 .expect("first flush must populate the entry");
13277 assert_eq!(after_first, std::time::Duration::from_millis(80));
13278
13279 inject_peer_for_flush(
13281 &mut actor,
13282 p,
13283 Some(std::time::Instant::now() - std::time::Duration::from_millis(40)),
13284 std::time::Duration::ZERO,
13285 );
13286 actor.disconnect_peer(p, "test");
13287 let after_second = *actor.unchoke_durations.get(&p).unwrap();
13288 assert!(
13289 after_second >= std::time::Duration::from_millis(120),
13290 "second flush must add to the existing entry, got {after_second:?}"
13291 );
13292 }
13293
13294 #[tokio::test]
13297 async fn piece_verified_wakes_reservation_notify() {
13298 let mut actor = TorrentActor::for_throttle_test(8, 0);
13299 let notify = Arc::new(tokio::sync::Notify::new());
13300 actor.reservation_notify = Some(Arc::clone(¬ify));
13301
13302 let notified = notify.notified();
13303 tokio::pin!(notified);
13304 assert!(
13305 futures::poll!(&mut notified).is_pending(),
13306 "notify should not have fired yet"
13307 );
13308
13309 actor.on_piece_verified(0).await;
13310
13311 tokio::time::timeout(Duration::from_secs(1), notified)
13312 .await
13313 .expect("reservation_notify must be woken by on_piece_verified");
13314 }
13315
13316 fn actor_with_tracker_state(queue: u32, inflight: u32) -> TorrentActor {
13322 use crate::piece_reservation::PieceTracker;
13323 use irontide_storage::Bitfield;
13324 let mut actor = TorrentActor::for_throttle_test(8, 0);
13325 let num_pieces = queue + inflight + 1;
13326 let we_have = Bitfield::new(num_pieces);
13327 let mut wanted = Bitfield::new(num_pieces);
13328 for i in 0..num_pieces {
13329 wanted.set(i);
13330 }
13331 let mut pt = PieceTracker::new(num_pieces, &we_have, &wanted);
13332 for i in queue..num_pieces {
13335 pt.mark_unwanted(i);
13336 }
13337 for i in 0..inflight {
13339 pt.record_reservation(i, "10.0.0.1:6881".parse().unwrap());
13340 }
13341 actor.piece_tracker = Some(pt);
13346 actor
13347 }
13348
13349 #[tokio::test]
13350 async fn pipeline_tick_skips_wake_when_dispatch_state_unchanged() {
13351 let mut actor = actor_with_tracker_state(10, 3);
13352 let notify = Arc::new(tokio::sync::Notify::new());
13353 actor.reservation_notify = Some(Arc::clone(¬ify));
13354
13355 actor.tick_dispatch_safety_wake();
13359 let _drain = notify.notified();
13360
13361 let notified = notify.notified();
13363 tokio::pin!(notified);
13364 actor.tick_dispatch_safety_wake();
13365
13366 tokio::task::yield_now().await;
13368 assert!(
13369 futures::poll!(&mut notified).is_pending(),
13370 "tick must not wake when (queue_count, inflight_count) is unchanged"
13371 );
13372 let skipped = actor.counters.get(crate::stats::DISPATCH_TICK_WAKE_SKIPPED);
13374 assert!(
13375 skipped >= 1,
13376 "expected DISPATCH_TICK_WAKE_SKIPPED >= 1, got {skipped}"
13377 );
13378 }
13379
13380 #[tokio::test]
13381 async fn pipeline_tick_wakes_when_inflight_changes() {
13382 let mut actor = actor_with_tracker_state(10, 3);
13383 let notify = Arc::new(tokio::sync::Notify::new());
13384 actor.reservation_notify = Some(Arc::clone(¬ify));
13385
13386 actor.tick_dispatch_safety_wake();
13388
13389 if let Some(ref mut pt) = actor.piece_tracker {
13392 pt.record_reservation(5, "10.0.0.2:6881".parse().unwrap());
13393 }
13394
13395 let notified = notify.notified();
13396 tokio::pin!(notified);
13397 actor.tick_dispatch_safety_wake();
13398
13399 tokio::time::timeout(Duration::from_secs(1), notified)
13400 .await
13401 .expect("tick must wake when dispatch state changed");
13402 }
13403
13404 fn inject_budget_peer(
13407 actor: &mut TorrentActor,
13408 peer_addr: std::net::SocketAddr,
13409 bytes_last_sec: u32,
13410 ) {
13411 let (cmd_tx, _cmd_rx) = mpsc::channel(8);
13412 let mut peer = crate::peer_state::PeerState::new(
13413 peer_addr,
13414 actor.num_pieces,
13415 cmd_tx,
13416 crate::peer_state::PeerSource::Tracker,
13417 Arc::new(AtomicU32::new(0)),
13418 Arc::new(AtomicU32::new(128)),
13419 Arc::new(tokio::sync::Notify::new()),
13420 );
13421 peer.peer_choking = false;
13422 peer.pipeline.block_received(bytes_last_sec);
13423 peer.pipeline.tick(); actor.peers.insert(peer_addr, peer);
13425 }
13426
13427 #[tokio::test]
13428 async fn m257c_pipeline_tick_reallocates_target_depth_by_rate() {
13429 use std::sync::atomic::Ordering::Relaxed;
13430
13431 let mut actor = TorrentActor::for_throttle_test(8, 0);
13432 actor.counters = Arc::new(crate::stats::SessionCounters::new_with_diagnostics(true));
13435 actor.config.request_budget_per_torrent = 144;
13436 actor.config.request_budget_floor = 8;
13437
13438 let fast = addr(1, 6881);
13439 let slow = addr(2, 6881);
13440 inject_budget_peer(&mut actor, fast, 30_000);
13442 inject_budget_peer(&mut actor, slow, 10_000);
13443
13444 actor.apply_request_budget();
13445
13446 let fast_q = actor.peers.get(&fast).unwrap().target_depth.load(Relaxed);
13447 let slow_q = actor.peers.get(&slow).unwrap().target_depth.load(Relaxed);
13448 assert!(
13449 fast_q > slow_q,
13450 "faster peer must get the larger quota (fast {fast_q} vs slow {slow_q})"
13451 );
13452 assert!(
13453 fast_q + slow_q <= 144,
13454 "quota sum {} must respect the budget 144",
13455 fast_q + slow_q
13456 );
13457 assert!(slow_q >= 8, "slow peer must hold the floor, got {slow_q}");
13458 assert!(
13459 fast_q <= crate::peer_shared::INITIAL_QUEUE_DEPTH as u32,
13460 "per-peer quota must stay capped, got {fast_q}"
13461 );
13462 let reallocs = actor.counters.get(crate::stats::BUDGET_REALLOCS_TOTAL);
13463 assert!(
13464 reallocs >= 1,
13465 "expected BUDGET_REALLOCS_TOTAL >= 1, got {reallocs}"
13466 );
13467
13468 actor.config.request_budget_per_torrent = 0;
13471 actor.apply_request_budget();
13472 let fast_q = actor.peers.get(&fast).unwrap().target_depth.load(Relaxed);
13473 let slow_q = actor.peers.get(&slow).unwrap().target_depth.load(Relaxed);
13474 assert_eq!(
13475 (fast_q, slow_q),
13476 (
13477 crate::peer_shared::INITIAL_QUEUE_DEPTH as u32,
13478 crate::peer_shared::INITIAL_QUEUE_DEPTH as u32
13479 ),
13480 "disabling the budget must restore legacy depth"
13481 );
13482 }
13483
13484 #[tokio::test]
13485 async fn m257f_bdp_cap_binds_target_depth_by_rtt() {
13486 use std::sync::atomic::Ordering::Relaxed;
13487
13488 let mut actor = TorrentActor::for_throttle_test(8, 0);
13492 actor.counters = Arc::new(crate::stats::SessionCounters::new_with_diagnostics(true));
13493 actor.config.request_budget_per_torrent = 1024; actor.config.request_budget_floor = 8;
13495
13496 let fast = addr(1, 6881);
13497 let cold = addr(2, 6881);
13498 inject_budget_peer(&mut actor, fast, 30_000);
13499 inject_budget_peer(&mut actor, cold, 30_000);
13500 {
13501 let p = actor.peers.get_mut(&fast).unwrap();
13502 p.pipeline.set_ewma_for_test(16_000_000.0);
13503 p.pipeline.set_window_for_test(16_000_000); p.avg_rtt = Some(0.160);
13505 }
13506 {
13507 let p = actor.peers.get_mut(&cold).unwrap();
13508 p.pipeline.set_ewma_for_test(16_000_000.0);
13509 p.pipeline.set_window_for_test(16_000_000);
13510 p.avg_rtt = None; }
13512
13513 actor.apply_request_budget();
13514
13515 let fast_q = actor.peers.get(&fast).unwrap().target_depth.load(Relaxed);
13517 let cold_q = actor.peers.get(&cold).unwrap().target_depth.load(Relaxed);
13518 assert_eq!(fast_q, 161, "measured peer binds at its BDP cap");
13519 assert_eq!(cold_q, 128, "cold peer stays at legacy init");
13520
13521 {
13524 let p = actor.peers.get_mut(&fast).unwrap();
13525 p.pipeline.set_ewma_for_test(2_000_000.0);
13526 p.pipeline.set_window_for_test(2_000_000);
13527 }
13528 actor.apply_request_budget();
13529 assert_eq!(
13530 actor.peers.get(&fast).unwrap().target_depth.load(Relaxed),
13531 161,
13532 "first shrink signal holds the prior cap"
13533 );
13534 actor.apply_request_budget();
13535 assert_eq!(
13536 actor.peers.get(&fast).unwrap().target_depth.load(Relaxed),
13537 161,
13538 "second shrink signal still holds"
13539 );
13540 actor.apply_request_budget();
13541 assert_eq!(
13543 actor.peers.get(&fast).unwrap().target_depth.load(Relaxed),
13544 24,
13545 "third consecutive shrink signal lands"
13546 );
13547
13548 {
13553 let p = actor.peers.get_mut(&fast).unwrap();
13554 p.peer_choking = true;
13555 p.pipeline.set_ewma_for_test(0.0);
13556 p.pipeline.set_window_for_test(0);
13557 }
13558 actor.apply_request_budget();
13559 actor.apply_request_budget();
13560 actor.apply_request_budget();
13561 assert_eq!(
13562 actor.peers.get(&fast).unwrap().bdp_cap,
13563 24,
13564 "choked peer's cap holds warm — no decay-driven shrink"
13565 );
13566 assert_eq!(
13567 actor.peers.get(&fast).unwrap().target_depth.load(Relaxed),
13568 8,
13569 "choked peer pinned at the probe floor while choked"
13570 );
13571
13572 actor.peers.get_mut(&fast).unwrap().peer_choking = false;
13577 actor.apply_request_budget();
13578 assert_eq!(
13579 actor.peers.get(&fast).unwrap().target_depth.load(Relaxed),
13580 24,
13581 "spill restores the warm cap's full quota immediately"
13582 );
13583
13584 actor.config.request_budget_per_torrent = 0;
13586 actor.apply_request_budget();
13587 assert_eq!(
13588 actor.peers.get(&fast).unwrap().target_depth.load(Relaxed),
13589 crate::request_budget::LEGACY_DEPTH
13590 );
13591 assert_eq!(
13592 actor.peers.get(&cold).unwrap().target_depth.load(Relaxed),
13593 crate::request_budget::LEGACY_DEPTH
13594 );
13595 }
13596
13597 #[tokio::test]
13598 async fn m257f_churn_suspends_bdp_pricing() {
13599 let mut actor = TorrentActor::for_throttle_test(8, 0);
13605 actor.config.request_budget_per_torrent = 1024;
13606 actor.config.request_budget_floor = 8;
13607 let a = addr(1, 6881);
13608 inject_budget_peer(&mut actor, a, 30_000);
13609 {
13610 let p = actor.peers.get_mut(&a).unwrap();
13611 p.pipeline.set_window_for_test(2_000_000);
13612 p.avg_rtt = Some(0.030);
13613 }
13614
13615 actor.apply_request_budget();
13618 actor.apply_request_budget();
13619 actor.apply_request_budget();
13620 assert_eq!(actor.peers.get(&a).unwrap().bdp_cap, 8);
13621 assert!(
13622 actor.rechoke_per_min_est.abs() < f64::EPSILON,
13623 "initial unchoke is a false-edge — never counted as churn"
13624 );
13625
13626 actor.rechoke_per_min_est = 50.0;
13629 actor.apply_request_budget();
13630 assert_eq!(
13631 actor.peers.get(&a).unwrap().bdp_cap,
13632 crate::request_budget::LEGACY_DEPTH,
13633 "churn suspension runs the permissive M257c regime"
13634 );
13635
13636 actor.rechoke_per_min_est = 0.0;
13639 actor.apply_request_budget();
13640 actor.apply_request_budget();
13641 actor.apply_request_budget();
13642 assert_eq!(actor.peers.get(&a).unwrap().bdp_cap, 8);
13643
13644 actor.peers.get_mut(&a).unwrap().peer_choking = true;
13648 actor.apply_request_budget();
13649 assert!(
13650 (actor.rechoke_per_min_est - 6.0).abs() < 1e-9,
13651 "one choke edge folds into the EWMA: {}",
13652 actor.rechoke_per_min_est
13653 );
13654
13655 actor.peers.get_mut(&a).unwrap().peer_choking = false;
13662 actor.peers.get_mut(&a).unwrap().prev_choking = false;
13663 actor.rechoke_per_min_est = 12.25;
13664 actor.apply_request_budget();
13665 assert!(
13666 actor.peers.get(&a).unwrap().bdp_cap < crate::request_budget::LEGACY_DEPTH,
13667 "high_bdp's ~12.25/min cycling must stay BDP-priced, not pin LEGACY (cap={})",
13668 actor.peers.get(&a).unwrap().bdp_cap
13669 );
13670 }
13671
13672 #[tokio::test]
13673 async fn m257f_block_rtt_sample_feeds_avg_rtt_ewma() {
13674 let mut actor = TorrentActor::for_throttle_test(8, 0);
13680 let addr1 = addr(1, 6881);
13681 inject_budget_peer(&mut actor, addr1, 30_000);
13682 assert_eq!(
13683 actor.peers.get(&addr1).unwrap().avg_rtt,
13684 None,
13685 "no block delivered yet — no RTT sample"
13686 );
13687
13688 actor
13690 .process_block_completion(addr1, 0, 0, 16384, Some(Duration::from_millis(200)))
13691 .await;
13692 let r1 = actor.peers.get(&addr1).unwrap().avg_rtt.unwrap();
13693 assert!((r1 - 0.200).abs() < 1e-9, "first sample seeds EWMA: {r1}");
13694
13695 actor
13697 .process_block_completion(addr1, 0, 16384, 16384, Some(Duration::from_millis(100)))
13698 .await;
13699 let r2 = actor.peers.get(&addr1).unwrap().avg_rtt.unwrap();
13700 assert!((r2 - 0.170).abs() < 1e-9, "EWMA blend alpha 0.3: {r2}");
13701
13702 actor
13704 .process_block_completion(addr1, 0, 32768, 16384, None)
13705 .await;
13706 let r3 = actor.peers.get(&addr1).unwrap().avg_rtt.unwrap();
13707 assert!((r3 - 0.170).abs() < 1e-9, "None sample must not move it");
13708 }
13709}