1use std::collections::{BTreeSet, HashMap, HashSet};
8use std::net::SocketAddr;
9
10use rustc_hash::FxHashMap;
11use std::sync::Arc;
12use std::sync::atomic::AtomicU32;
13use std::time::{Duration, Instant};
14
15use bytes::Bytes;
16use tokio::sync::{broadcast, mpsc, oneshot};
17use tracing::{debug, info, trace, warn};
18
19use crate::alert::{Alert, AlertKind, post_alert};
20use crate::disk::{DiskHandle, DiskJobFlags, DiskManagerHandle};
21use crate::piece_reservation::{
22 AtomicPieceStates, AvailabilitySnapshot, BlockMaps, StealCandidates,
23};
24
25use irontide_core::{
26 DEFAULT_CHUNK_SIZE, FilePriority, Id20, Lengths, Magnet, PeerId, TorrentMetaV1,
27 torrent_from_bytes,
28};
29use irontide_dht::DhtHandle;
30use irontide_storage::{Bitfield, ChunkTracker, MemoryStorage, TorrentStorage};
31
32use crate::choker::{Choker, PeerInfo as ChokerPeerInfo};
33use crate::end_game::EndGame;
34use crate::metadata::MetadataDownloader;
35use crate::peer_adder::{self, ConnectPeer};
36use crate::peer_state::{PeerSource, PeerState};
37use crate::tracker_manager::TrackerManager;
38use crate::types::{
39 PartialPieceInfo, PeerCommand, PeerEvent, PeerInfo, TorrentCommand, TorrentConfig,
40 TorrentState, TorrentStats,
41};
42
43pub(crate) type SharedBucket = Arc<parking_lot::Mutex<crate::rate_limiter::TokenBucket>>;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub(crate) enum HashResult {
53 Passed,
55 Failed,
57 NotApplicable,
59}
60
61pub(crate) fn relocate_files(
67 src_base: &std::path::Path,
68 dst_base: &std::path::Path,
69 file_paths: &[std::path::PathBuf],
70) -> std::io::Result<()> {
71 for rel_path in file_paths {
72 let src = src_base.join(rel_path);
73 let dst = dst_base.join(rel_path);
74
75 if !src.exists() {
76 continue;
78 }
79
80 if let Some(parent) = dst.parent() {
81 std::fs::create_dir_all(parent)?;
82 }
83
84 if std::fs::rename(&src, &dst).is_err() {
86 std::fs::copy(&src, &dst)?;
88 std::fs::remove_file(&src)?;
89 }
90 }
91
92 for rel_path in file_paths {
95 let mut dir = src_base.join(rel_path);
96 dir.pop(); while dir != *src_base {
98 if std::fs::remove_dir(&dir).is_err() {
99 break; }
101 dir.pop();
102 }
103 }
104
105 Ok(())
106}
107
108pub(crate) fn now_unix() -> i64 {
110 std::time::SystemTime::now()
111 .duration_since(std::time::UNIX_EPOCH)
112 .map(|d| d.as_secs() as i64)
113 .unwrap_or(0)
114}
115
116pub(crate) const HOLEPUNCH_COOLDOWN: Duration = Duration::from_secs(120);
118
119pub(crate) const HOLEPUNCH_MAX_TRACKED: usize = 256;
121
122pub(crate) fn should_attempt_holepunch(reason: &str) -> bool {
125 if reason.contains("holepunch") {
127 return false;
128 }
129 reason.contains("refused")
130 || reason.contains("timed out")
131 || reason.contains("Connection reset")
132 || reason.contains("connection reset")
133}
134
135#[derive(Clone)]
137pub struct TorrentHandle {
138 cmd_tx: mpsc::Sender<TorrentCommand>,
139}
140
141impl TorrentHandle {
142 #[allow(clippy::too_many_arguments)]
146 pub(crate) async fn from_torrent(
147 meta: TorrentMetaV1,
148 version: irontide_core::TorrentVersion,
149 meta_v2: Option<irontide_core::TorrentMetaV2>,
150 disk: DiskHandle,
151 disk_manager: DiskManagerHandle,
152 config: TorrentConfig,
153 dht: Option<DhtHandle>,
154 dht_v6: Option<DhtHandle>,
155 global_upload_bucket: Option<SharedBucket>,
156 global_download_bucket: Option<SharedBucket>,
157 slot_tuner: crate::slot_tuner::SlotTuner,
158 alert_tx: broadcast::Sender<Alert>,
159 alert_mask: Arc<AtomicU32>,
160 utp_socket: Option<irontide_utp::UtpSocket>,
161 utp_socket_v6: Option<irontide_utp::UtpSocket>,
162 ban_manager: crate::session::SharedBanManager,
163 ip_filter: crate::session::SharedIpFilter,
164 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
165 sam_session: Option<Arc<crate::i2p::SamSession>>,
166 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
167 factory: Arc<crate::transport::NetworkFactory>,
168 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
169 ) -> crate::Result<Self> {
170 let mut config = config;
171 if meta.info.private == Some(1) {
173 config.enable_dht = false;
174 config.enable_pex = false;
175 config.enable_lsd = false;
176 }
177
178 let info_hashes = match (&version, &meta_v2) {
179 (irontide_core::TorrentVersion::Hybrid, Some(v2_meta)) => {
180 if let Some(v2_hash) = v2_meta.info_hashes.v2 {
181 irontide_core::InfoHashes::hybrid(meta.info_hash, v2_hash)
182 } else {
183 irontide_core::InfoHashes::v1_only(meta.info_hash)
184 }
185 }
186 (irontide_core::TorrentVersion::V2Only, Some(v2_meta)) => v2_meta.info_hashes.clone(),
187 _ => irontide_core::InfoHashes::v1_only(meta.info_hash),
188 };
189
190 if meta.info.piece_length > config.max_piece_length {
191 return Err(crate::Error::InvalidSettings(format!(
192 "piece_length {} exceeds max_piece_length {}",
193 meta.info.piece_length, config.max_piece_length
194 )));
195 }
196
197 let num_pieces = meta.info.num_pieces() as u32;
198 let lengths = Lengths::new(
199 meta.info.total_length(),
200 meta.info.piece_length,
201 DEFAULT_CHUNK_SIZE,
202 );
203 let mut chunk_tracker = ChunkTracker::new(lengths.clone());
204
205 let hash_picker = if version.has_v2() {
207 if let Some(ref v2_meta) = meta_v2 {
208 chunk_tracker.enable_v2_tracking();
209
210 let block_size = 16384u64;
211 let blocks_per_piece = (meta.info.piece_length / block_size) as u32;
212
213 let v2_files = v2_meta.info.files();
215 let file_infos: Vec<irontide_core::FileHashInfo> = v2_files
216 .iter()
217 .filter_map(|f| {
218 let root = f.attr.pieces_root?;
219 let num_blocks = f.attr.length.div_ceil(block_size) as u32;
220 let num_pieces = f.attr.length.div_ceil(meta.info.piece_length) as u32;
221 Some(irontide_core::FileHashInfo {
222 root,
223 num_blocks,
224 num_pieces,
225 })
226 })
227 .collect();
228
229 if !file_infos.is_empty() {
230 let mut picker = irontide_core::HashPicker::new(&file_infos, blocks_per_piece);
231
232 let _verified = picker.load_piece_layers(&v2_meta.piece_layers);
234
235 Some(picker)
236 } else {
237 None
238 }
239 } else {
240 None
241 }
242 } else {
243 None
244 };
245
246 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
247 let file_priorities = vec![FilePriority::Normal; file_lengths.len()];
248 let wanted_pieces =
249 crate::piece_selector::build_wanted_pieces(&file_priorities, &file_lengths, &lengths);
250
251 let (cmd_tx, cmd_rx) = mpsc::channel(256);
252 let (event_tx, event_rx) = mpsc::channel(2048);
253 let (write_error_tx, write_error_rx) = mpsc::channel(64);
254 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
255 let (hash_result_tx, hash_result_rx) = mpsc::channel(64); let our_peer_id = if config.anonymous_mode {
257 PeerId::generate_anonymous().0
258 } else {
259 PeerId::generate().0
260 };
261
262 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
265 .bind_tcp(SocketAddr::from((
266 std::net::Ipv6Addr::UNSPECIFIED,
267 config.listen_port,
268 )))
269 .await
270 {
271 Ok(l) => Some(l),
272 Err(_) => factory
273 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
274 .await
275 .ok(),
276 };
277 let mut tracker_manager = TrackerManager::from_torrent_filtered(
280 &meta,
281 our_peer_id,
282 config.listen_port,
283 &config.url_security,
284 config.peer_dscp,
285 config.anonymous_mode,
286 );
287 tracker_manager.set_info_hashes(info_hashes.clone());
288
289 if let Some(ref sam) = sam_session {
291 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
292 }
293
294 let enable_dht = config.enable_dht;
295
296 let dht_peers_rx = if enable_dht {
298 if let Some(ref dht) = dht {
299 match dht.get_peers(meta.info_hash).await {
300 Ok(rx) => Some(rx),
301 Err(e) => {
302 warn!("failed to start DHT v4 get_peers: {e}");
303 None
304 }
305 }
306 } else {
307 None
308 }
309 } else {
310 None
311 };
312
313 let dht_v6_peers_rx = if enable_dht {
314 if let Some(ref dht6) = dht_v6 {
315 match dht6.get_peers(meta.info_hash).await {
316 Ok(rx) => Some(rx),
317 Err(e) => {
318 debug!("failed to start DHT v6 get_peers: {e}");
319 None
320 }
321 }
322 } else {
323 None
324 }
325 } else {
326 None
327 };
328
329 let v2_as_v1 = if info_hashes.is_hybrid() {
331 info_hashes
332 .v2
333 .map(|v2| Id20(v2.0[..20].try_into().unwrap()))
334 } else {
335 None
336 };
337 let (dht_v2_peers_rx, dht_v6_v2_peers_rx) =
338 if let (true, Some(v2_id)) = (enable_dht, v2_as_v1) {
339 let rx4 = if let Some(ref dht) = dht {
340 dht.get_peers(v2_id).await.ok()
341 } else {
342 None
343 };
344 let rx6 = if let Some(ref dht6) = dht_v6 {
345 dht6.get_peers(v2_id).await.ok()
346 } else {
347 None
348 };
349 (rx4, rx6)
350 } else {
351 (None, None)
352 };
353
354 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
355 let download_bucket = crate::rate_limiter::TokenBucket::new(config.download_rate_limit);
356 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
357 0,
358 0,
359 0,
360 0,
361 config.upload_rate_limit,
362 config.download_rate_limit,
363 );
364
365 let super_seed = if config.super_seeding {
366 Some(crate::super_seed::SuperSeedState::new())
367 } else {
368 None
369 };
370 let (have_broadcast_tx, _) =
372 tokio::sync::broadcast::channel(std::cmp::max(128, num_pieces as usize / 4));
373 let is_share_mode = config.share_mode;
374
375 let (piece_ready_tx, _) = broadcast::channel(64);
376 let initial_have = chunk_tracker.bitfield().clone();
377 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(initial_have);
378 let stream_read_semaphore =
379 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
380
381 let choker = Choker::with_algorithms(
382 4,
383 config.seed_choking_algorithm,
384 config.choking_algorithm,
385 config.upload_rate_limit,
386 2,
387 20,
388 );
389
390 let mut disk = disk;
392 if matches!(version, irontide_core::TorrentVersion::V1Only)
393 && let Some(pool) = &hash_pool
394 {
395 disk.set_hash_pool(pool.clone());
396 disk.set_hash_result_tx(hash_result_tx.clone());
397 }
398
399 let cached_files = Some(build_cached_file_info(&meta, &lengths));
401
402 let actor = TorrentActor {
403 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
404 config.lock_warn_threshold_ms,
405 ),
406 config,
407 info_hash: meta.info_hash,
408 our_peer_id,
409 state: TorrentState::Downloading,
410 disk: Some(disk),
411 disk_manager,
412 chunk_tracker: Some(chunk_tracker),
413 lengths: Some(lengths),
414 num_pieces,
415 streaming_pieces: BTreeSet::new(),
416 time_critical_pieces: BTreeSet::new(),
417 streaming_cursors: Vec::new(),
418 piece_ready_tx,
419 have_watch_tx,
420 have_watch_rx,
421 stream_read_semaphore,
422 file_priorities,
423 wanted_pieces,
424 end_game: EndGame::new(),
425 peers: HashMap::new(),
426 cached_peer_rates: FxHashMap::default(),
427 refill_notify: Arc::new(tokio::sync::Notify::new()),
428 atomic_states: None,
429 block_maps: None,
430 steal_candidates: None,
431 last_steal_populate: Instant::now(),
432 piece_write_guards: None,
433 snapshot_dirty: false,
434 availability_snapshot: None,
435 snapshot_generation: 0,
436 piece_owner: Vec::new(),
437 peer_slab: crate::piece_reservation::PeerSlab::new(),
438 availability: Vec::new(),
439 priority_pieces: BTreeSet::new(),
440 max_in_flight: 512,
441 reservation_notify: None,
442 choker,
443 user_seed_mode: false,
444 max_connections: 0,
445 peer_states: None,
446 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
447 connect_permits: HashMap::new(),
448 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
449 std::collections::HashSet::new(),
450 )),
451 connect_rx: None,
452 metadata_downloader: None,
453 downloaded: 0,
454 uploaded: 0,
455 checking_progress: 0.0,
456 total_download: 0,
457 total_upload: 0,
458 total_failed_bytes: 0,
459 total_redundant_bytes: 0,
460 added_time: std::time::SystemTime::now()
461 .duration_since(std::time::UNIX_EPOCH)
462 .map(|d| d.as_secs() as i64)
463 .unwrap_or(0),
464 completed_time: 0,
465 last_download: 0,
466 last_upload: 0,
467 last_seen_complete: 0,
468 active_duration: 0,
469 finished_duration: 0,
470 seeding_duration: 0,
471 active_since: Some(std::time::Instant::now()),
472 state_duration_since: None,
473 started_at: std::time::Instant::now(),
474 moving_storage: false,
475 has_incoming: false,
476 need_save_resume: false,
477 error: String::new(),
478 error_file: -1,
479 cmd_rx,
480 event_tx,
481 event_rx,
482 write_error_rx,
483 write_error_tx,
484 verify_result_rx,
485 verify_result_tx,
486 pending_verify: HashSet::new(),
487 piece_generations: vec![0u64; num_pieces as usize],
488 hash_result_rx,
489 hash_result_tx,
490 meta: Some(meta),
491 cached_files,
492 listener,
493 utp_socket,
494 utp_socket_v6,
495 tracker_manager,
496 tracker_result_rx: None,
497 dht: if enable_dht { dht } else { None },
498 dht_v6: if enable_dht { dht_v6 } else { None },
499 dht_peers_rx,
500 dht_v6_peers_rx,
501 dht_v6_empty_count: 0,
502 dht_v6_last_retry: None,
503 alert_tx,
504 alert_mask,
505 upload_bucket,
506 download_bucket,
507 global_upload_bucket,
508 global_download_bucket,
509 slot_tuner,
510 upload_bytes_interval: 0,
511 peak_download_rate: 0,
512 web_seeds: HashMap::new(),
513 banned_web_seeds: HashSet::new(),
514 web_seed_in_flight: HashMap::new(),
515 super_seed,
516 have_broadcast_tx,
517 suggested_to_peers: HashMap::new(),
518 predictive_have_sent: HashSet::new(),
519
520 ban_manager,
521 ip_filter,
522 piece_contributors: HashMap::new(),
523 parole_pieces: HashMap::new(),
524 external_ip: None,
525 share_lru: std::collections::VecDeque::new(),
526 share_max_pieces: if is_share_mode { 64 } else { 0 },
527 plugins,
528 hash_picker,
529 version,
530 meta_v2,
531 info_hashes,
532 dht_v2_peers_rx,
533 dht_v6_v2_peers_rx,
534 magnet_selected_files: None,
535 sam_session,
536 i2p_accept_rx: None,
537 i2p_peer_counter: 0,
538 i2p_destinations: HashMap::new(),
539 ssl_manager,
540 rate_limiter_set,
541 auto_sequential_active: false,
542 factory,
543 hash_pool_ref: hash_pool,
544 connect_attempts: 0,
545 connect_failures: 0,
546 choke_rotations: 0,
547 inflight_started: Vec::new(),
548 completed_piece_times: std::collections::VecDeque::new(),
549 piece_steals: 0,
550 holepunch_cooldowns: HashMap::new(),
551 holepunch_pending: Vec::new(),
552 };
553
554 let spawn_info_hash = actor.info_hash;
555 let join_handle = tokio::spawn(actor.run());
556 tokio::spawn(async move {
558 match join_handle.await {
559 Ok(()) => {
560 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
561 }
562 Err(e) if e.is_panic() => {
563 let panic_payload = e.into_panic();
564 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
565 (*s).to_string()
566 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
567 s.clone()
568 } else {
569 "unknown panic payload".to_string()
570 };
571 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
572 }
573 Err(e) => {
574 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
575 }
576 }
577 });
578 Ok(TorrentHandle { cmd_tx })
579 }
580
581 #[allow(clippy::too_many_arguments)]
583 pub(crate) async fn from_magnet(
584 magnet: Magnet,
585 disk_manager: DiskManagerHandle,
586 config: TorrentConfig,
587 dht: Option<DhtHandle>,
588 dht_v6: Option<DhtHandle>,
589 global_upload_bucket: Option<SharedBucket>,
590 global_download_bucket: Option<SharedBucket>,
591 slot_tuner: crate::slot_tuner::SlotTuner,
592 alert_tx: broadcast::Sender<Alert>,
593 alert_mask: Arc<AtomicU32>,
594 utp_socket: Option<irontide_utp::UtpSocket>,
595 utp_socket_v6: Option<irontide_utp::UtpSocket>,
596 ban_manager: crate::session::SharedBanManager,
597 ip_filter: crate::session::SharedIpFilter,
598 plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
599 sam_session: Option<Arc<crate::i2p::SamSession>>,
600 ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
601 factory: Arc<crate::transport::NetworkFactory>,
602 hash_pool: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
603 ) -> crate::Result<Self> {
604 let (cmd_tx, cmd_rx) = mpsc::channel(256);
605 let (event_tx, event_rx) = mpsc::channel(2048);
606 let (write_error_tx, write_error_rx) = mpsc::channel(64);
607 let (verify_result_tx, verify_result_rx) = mpsc::channel(1024);
608 let (hash_result_tx, hash_result_rx) = mpsc::channel(1);
610 let our_peer_id = if config.anonymous_mode {
611 PeerId::generate_anonymous().0
612 } else {
613 PeerId::generate().0
614 };
615
616 let listener: Option<Box<dyn crate::transport::TransportListener>> = match factory
618 .bind_tcp(SocketAddr::from((
619 std::net::Ipv6Addr::UNSPECIFIED,
620 config.listen_port,
621 )))
622 .await
623 {
624 Ok(l) => Some(l),
625 Err(_) => factory
626 .bind_tcp(SocketAddr::from(([0, 0, 0, 0], config.listen_port)))
627 .await
628 .ok(),
629 };
630 let mut tracker_manager = TrackerManager::empty(
633 magnet.info_hash(),
634 our_peer_id,
635 config.listen_port,
636 config.peer_dscp,
637 config.anonymous_mode,
638 );
639 for url in &magnet.trackers {
641 tracker_manager.add_tracker_url(url);
642 }
643
644 if let Some(ref sam) = sam_session {
646 tracker_manager.set_i2p_destination(Some(sam.destination().to_base64()));
647 }
648
649 let enable_dht = config.enable_dht;
650
651 let dht_peers_rx = if enable_dht {
653 if let Some(ref dht) = dht {
654 match dht.get_peers(magnet.info_hash()).await {
655 Ok(rx) => Some(rx),
656 Err(e) => {
657 warn!("failed to start DHT v4 get_peers: {e}");
658 None
659 }
660 }
661 } else {
662 None
663 }
664 } else {
665 None
666 };
667
668 let dht_v6_peers_rx = if enable_dht {
669 if let Some(ref dht6) = dht_v6 {
670 match dht6.get_peers(magnet.info_hash()).await {
671 Ok(rx) => Some(rx),
672 Err(e) => {
673 debug!("failed to start DHT v6 get_peers: {e}");
674 None
675 }
676 }
677 } else {
678 None
679 }
680 } else {
681 None
682 };
683
684 let upload_bucket = crate::rate_limiter::TokenBucket::new(config.upload_rate_limit);
685 let download_bucket = crate::rate_limiter::TokenBucket::new(config.download_rate_limit);
686 let rate_limiter_set = crate::rate_limiter::RateLimiterSet::new(
687 0,
688 0,
689 0,
690 0,
691 config.upload_rate_limit,
692 config.download_rate_limit,
693 );
694
695 let super_seed = if config.super_seeding {
696 Some(crate::super_seed::SuperSeedState::new())
697 } else {
698 None
699 };
700 let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
702 let is_share_mode = config.share_mode;
703 let magnet_selected_files = magnet.selected_files.clone();
704 let info_hashes = magnet.info_hashes.clone();
705
706 let (piece_ready_tx, _) = broadcast::channel(64);
707 let (have_watch_tx, have_watch_rx) = tokio::sync::watch::channel(Bitfield::new(0));
708 let stream_read_semaphore =
709 crate::streaming::stream_read_semaphore(config.max_concurrent_stream_reads);
710
711 let choker = Choker::with_algorithms(
712 4,
713 config.seed_choking_algorithm,
714 config.choking_algorithm,
715 config.upload_rate_limit,
716 2,
717 20,
718 );
719
720 let actor = TorrentActor {
721 lock_timing: crate::timed_lock::LockTimingSettings::from_ms(
722 config.lock_warn_threshold_ms,
723 ),
724 config,
725 info_hash: magnet.info_hash(),
726 our_peer_id,
727 state: TorrentState::FetchingMetadata,
728 disk: None,
729 disk_manager,
730 chunk_tracker: None,
731 lengths: None,
732 num_pieces: 0,
733 streaming_pieces: BTreeSet::new(),
734 time_critical_pieces: BTreeSet::new(),
735 streaming_cursors: Vec::new(),
736 piece_ready_tx,
737 have_watch_tx,
738 have_watch_rx,
739 stream_read_semaphore,
740 file_priorities: Vec::new(),
741 wanted_pieces: Bitfield::new(0),
742 end_game: EndGame::new(),
743 peers: HashMap::new(),
744 cached_peer_rates: FxHashMap::default(),
745 refill_notify: Arc::new(tokio::sync::Notify::new()),
746 atomic_states: None,
747 block_maps: None,
748 steal_candidates: None,
749 last_steal_populate: Instant::now(),
750 piece_write_guards: None,
751 snapshot_dirty: false,
752 availability_snapshot: None,
753 snapshot_generation: 0,
754 piece_owner: Vec::new(),
755 peer_slab: crate::piece_reservation::PeerSlab::new(),
756 availability: Vec::new(),
757 priority_pieces: BTreeSet::new(),
758 max_in_flight: 512,
759 reservation_notify: None,
760 choker,
761 user_seed_mode: false,
762 max_connections: 0,
763 peer_states: None,
764 connect_semaphore: Arc::new(tokio::sync::Semaphore::new(0)),
765 connect_permits: HashMap::new(),
766 live_outgoing_peers: std::sync::Arc::new(parking_lot::RwLock::new(
767 std::collections::HashSet::new(),
768 )),
769 connect_rx: None,
770 metadata_downloader: Some(MetadataDownloader::new(magnet.info_hash())),
771 downloaded: 0,
772 uploaded: 0,
773 checking_progress: 0.0,
774 total_download: 0,
775 total_upload: 0,
776 total_failed_bytes: 0,
777 total_redundant_bytes: 0,
778 added_time: std::time::SystemTime::now()
779 .duration_since(std::time::UNIX_EPOCH)
780 .map(|d| d.as_secs() as i64)
781 .unwrap_or(0),
782 completed_time: 0,
783 last_download: 0,
784 last_upload: 0,
785 last_seen_complete: 0,
786 active_duration: 0,
787 finished_duration: 0,
788 seeding_duration: 0,
789 active_since: Some(std::time::Instant::now()),
790 state_duration_since: None,
791 started_at: std::time::Instant::now(),
792 moving_storage: false,
793 has_incoming: false,
794 need_save_resume: false,
795 error: String::new(),
796 error_file: -1,
797 cmd_rx,
798 event_tx,
799 event_rx,
800 write_error_rx,
801 write_error_tx,
802 verify_result_rx,
803 verify_result_tx,
804 pending_verify: HashSet::new(),
805 piece_generations: Vec::new(),
806 hash_result_rx,
807 hash_result_tx,
808 meta: None,
809 cached_files: None,
810 listener,
811 utp_socket,
812 utp_socket_v6,
813 tracker_manager,
814 tracker_result_rx: None,
815 dht: if enable_dht { dht } else { None },
816 dht_v6: if enable_dht { dht_v6 } else { None },
817 dht_peers_rx,
818 dht_v6_peers_rx,
819 dht_v6_empty_count: 0,
820 dht_v6_last_retry: None,
821 alert_tx,
822 alert_mask,
823 upload_bucket,
824 download_bucket,
825 global_upload_bucket,
826 global_download_bucket,
827 slot_tuner,
828 upload_bytes_interval: 0,
829 peak_download_rate: 0,
830 web_seeds: HashMap::new(),
831 banned_web_seeds: HashSet::new(),
832 web_seed_in_flight: HashMap::new(),
833 super_seed,
834 have_broadcast_tx,
835 suggested_to_peers: HashMap::new(),
836 predictive_have_sent: HashSet::new(),
837
838 ban_manager,
839 ip_filter,
840 piece_contributors: HashMap::new(),
841 parole_pieces: HashMap::new(),
842 external_ip: None,
843 share_lru: std::collections::VecDeque::new(),
844 share_max_pieces: if is_share_mode { 64 } else { 0 },
845 plugins,
846 hash_picker: None,
847 version: irontide_core::TorrentVersion::V1Only,
848 meta_v2: None,
849 info_hashes,
850 dht_v2_peers_rx: None,
851 dht_v6_v2_peers_rx: None,
852 magnet_selected_files,
853 sam_session,
854 i2p_accept_rx: None,
855 i2p_peer_counter: 0,
856 i2p_destinations: HashMap::new(),
857 ssl_manager,
858 rate_limiter_set,
859 auto_sequential_active: false,
860 factory,
861 hash_pool_ref: hash_pool,
862 connect_attempts: 0,
863 connect_failures: 0,
864 choke_rotations: 0,
865 inflight_started: Vec::new(),
866 completed_piece_times: std::collections::VecDeque::new(),
867 piece_steals: 0,
868 holepunch_cooldowns: HashMap::new(),
869 holepunch_pending: Vec::new(),
870 };
871
872 let spawn_info_hash = actor.info_hash;
873 let join_handle = tokio::spawn(actor.run());
874 tokio::spawn(async move {
875 match join_handle.await {
876 Ok(()) => {
877 tracing::warn!(%spawn_info_hash, "torrent actor exited cleanly");
878 }
879 Err(e) if e.is_panic() => {
880 let panic_payload = e.into_panic();
881 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
882 (*s).to_string()
883 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
884 s.clone()
885 } else {
886 "unknown panic payload".to_string()
887 };
888 tracing::error!(%spawn_info_hash, "torrent actor PANICKED: {msg}");
889 }
890 Err(e) => {
891 tracing::error!(%spawn_info_hash, "torrent actor task error: {e}");
892 }
893 }
894 });
895 Ok(TorrentHandle { cmd_tx })
896 }
897
898 pub(crate) async fn send_incoming_peer(
900 &self,
901 stream: crate::transport::BoxedStream,
902 addr: SocketAddr,
903 ) -> crate::Result<()> {
904 self.cmd_tx
905 .send(TorrentCommand::IncomingPeer { stream, addr })
906 .await
907 .map_err(|_| crate::Error::Shutdown)
908 }
909
910 pub async fn stats(&self) -> crate::Result<TorrentStats> {
912 let (tx, rx) = oneshot::channel();
913 self.cmd_tx
914 .send(TorrentCommand::Stats { reply: tx })
915 .await
916 .map_err(|_| crate::Error::Shutdown)?;
917 rx.await.map_err(|_| crate::Error::Shutdown)
918 }
919
920 pub async fn add_peers(&self, peers: Vec<SocketAddr>, source: PeerSource) -> crate::Result<()> {
922 self.cmd_tx
923 .send(TorrentCommand::AddPeers { peers, source })
924 .await
925 .map_err(|_| crate::Error::Shutdown)
926 }
927
928 pub async fn pause(&self) -> crate::Result<()> {
930 self.cmd_tx
931 .send(TorrentCommand::Pause)
932 .await
933 .map_err(|_| crate::Error::Shutdown)
934 }
935
936 pub async fn resume(&self) -> crate::Result<()> {
938 self.cmd_tx
939 .send(TorrentCommand::Resume)
940 .await
941 .map_err(|_| crate::Error::Shutdown)
942 }
943
944 pub async fn shutdown(&self) -> crate::Result<()> {
946 let _ = tokio::time::timeout(
949 std::time::Duration::from_secs(5),
950 self.cmd_tx.send(TorrentCommand::Shutdown),
951 )
952 .await;
953 Ok(())
954 }
955
956 pub async fn save_resume_data(&self) -> crate::Result<irontide_core::FastResumeData> {
958 let (tx, rx) = oneshot::channel();
959 self.cmd_tx
960 .send(TorrentCommand::SaveResumeData { reply: tx })
961 .await
962 .map_err(|_| crate::Error::Shutdown)?;
963 rx.await.map_err(|_| crate::Error::Shutdown)?
964 }
965
966 pub(crate) async fn clear_save_resume_flag(&self) -> crate::Result<()> {
968 self.cmd_tx
969 .send(TorrentCommand::ClearSaveResumeFlag)
970 .await
971 .map_err(|_| crate::Error::Shutdown)
972 }
973
974 pub(crate) async fn restore_resume_bitmap(&self, pieces: Vec<u8>) -> crate::Result<()> {
985 let (tx, rx) = oneshot::channel();
986 self.cmd_tx
987 .send(TorrentCommand::RestoreResumeBitmap { pieces, reply: tx })
988 .await
989 .map_err(|_| crate::Error::Shutdown)?;
990 rx.await.map_err(|_| crate::Error::Shutdown)?
991 }
992
993 pub async fn set_file_priority(
995 &self,
996 index: usize,
997 priority: irontide_core::FilePriority,
998 ) -> crate::Result<()> {
999 let (tx, rx) = oneshot::channel();
1000 self.cmd_tx
1001 .send(TorrentCommand::SetFilePriority {
1002 index,
1003 priority,
1004 reply: tx,
1005 })
1006 .await
1007 .map_err(|_| crate::Error::Shutdown)?;
1008 rx.await.map_err(|_| crate::Error::Shutdown)?
1009 }
1010
1011 pub async fn file_priorities(&self) -> crate::Result<Vec<irontide_core::FilePriority>> {
1013 let (tx, rx) = oneshot::channel();
1014 self.cmd_tx
1015 .send(TorrentCommand::FilePriorities { reply: tx })
1016 .await
1017 .map_err(|_| crate::Error::Shutdown)?;
1018 rx.await.map_err(|_| crate::Error::Shutdown)
1019 }
1020
1021 pub async fn tracker_list(&self) -> crate::Result<Vec<crate::tracker_manager::TrackerInfo>> {
1023 let (tx, rx) = oneshot::channel();
1024 self.cmd_tx
1025 .send(TorrentCommand::TrackerList { reply: tx })
1026 .await
1027 .map_err(|_| crate::Error::Shutdown)?;
1028 rx.await.map_err(|_| crate::Error::Shutdown)
1029 }
1030
1031 pub async fn force_reannounce(&self) -> crate::Result<()> {
1033 self.cmd_tx
1034 .send(TorrentCommand::ForceReannounce)
1035 .await
1036 .map_err(|_| crate::Error::Shutdown)
1037 }
1038
1039 pub async fn scrape(&self) -> crate::Result<Option<(String, irontide_tracker::ScrapeInfo)>> {
1041 let (tx, rx) = oneshot::channel();
1042 self.cmd_tx
1043 .send(TorrentCommand::Scrape { reply: tx })
1044 .await
1045 .map_err(|_| crate::Error::Shutdown)?;
1046 rx.await.map_err(|_| crate::Error::Shutdown)
1047 }
1048
1049 pub async fn open_file(
1051 &self,
1052 file_index: usize,
1053 ) -> crate::Result<crate::streaming::FileStream> {
1054 let (tx, rx) = oneshot::channel();
1055 self.cmd_tx
1056 .send(TorrentCommand::OpenFile {
1057 file_index,
1058 reply: tx,
1059 })
1060 .await
1061 .map_err(|_| crate::Error::Shutdown)?;
1062 let handle = rx.await.map_err(|_| crate::Error::Shutdown)??;
1063 Ok(crate::streaming::FileStream::from_handle(handle))
1064 }
1065
1066 pub(crate) async fn update_external_ip(&self, ip: std::net::IpAddr) -> crate::Result<()> {
1068 self.cmd_tx
1069 .send(TorrentCommand::UpdateExternalIp { ip })
1070 .await
1071 .map_err(|_| crate::Error::Shutdown)
1072 }
1073
1074 pub async fn move_storage(&self, new_path: std::path::PathBuf) -> crate::Result<()> {
1079 let (tx, rx) = oneshot::channel();
1080 self.cmd_tx
1081 .send(TorrentCommand::MoveStorage {
1082 new_path,
1083 reply: tx,
1084 })
1085 .await
1086 .map_err(|_| crate::Error::Shutdown)?;
1087 rx.await.map_err(|_| crate::Error::Shutdown)?
1088 }
1089
1090 pub async fn set_download_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1092 let (tx, rx) = oneshot::channel();
1093 self.cmd_tx
1094 .send(TorrentCommand::SetDownloadLimit {
1095 bytes_per_sec,
1096 reply: tx,
1097 })
1098 .await
1099 .map_err(|_| crate::Error::Shutdown)?;
1100 rx.await.map_err(|_| crate::Error::Shutdown)
1101 }
1102
1103 pub async fn set_upload_limit(&self, bytes_per_sec: u64) -> crate::Result<()> {
1105 let (tx, rx) = oneshot::channel();
1106 self.cmd_tx
1107 .send(TorrentCommand::SetUploadLimit {
1108 bytes_per_sec,
1109 reply: tx,
1110 })
1111 .await
1112 .map_err(|_| crate::Error::Shutdown)?;
1113 rx.await.map_err(|_| crate::Error::Shutdown)
1114 }
1115
1116 pub async fn download_limit(&self) -> crate::Result<u64> {
1118 let (tx, rx) = oneshot::channel();
1119 self.cmd_tx
1120 .send(TorrentCommand::DownloadLimit { reply: tx })
1121 .await
1122 .map_err(|_| crate::Error::Shutdown)?;
1123 rx.await.map_err(|_| crate::Error::Shutdown)
1124 }
1125
1126 pub async fn upload_limit(&self) -> crate::Result<u64> {
1128 let (tx, rx) = oneshot::channel();
1129 self.cmd_tx
1130 .send(TorrentCommand::UploadLimit { reply: tx })
1131 .await
1132 .map_err(|_| crate::Error::Shutdown)?;
1133 rx.await.map_err(|_| crate::Error::Shutdown)
1134 }
1135
1136 pub async fn set_sequential_download(&self, enabled: bool) -> crate::Result<()> {
1138 let (tx, rx) = oneshot::channel();
1139 self.cmd_tx
1140 .send(TorrentCommand::SetSequentialDownload { enabled, reply: tx })
1141 .await
1142 .map_err(|_| crate::Error::Shutdown)?;
1143 rx.await.map_err(|_| crate::Error::Shutdown)
1144 }
1145
1146 pub async fn is_sequential_download(&self) -> crate::Result<bool> {
1148 let (tx, rx) = oneshot::channel();
1149 self.cmd_tx
1150 .send(TorrentCommand::IsSequentialDownload { reply: tx })
1151 .await
1152 .map_err(|_| crate::Error::Shutdown)?;
1153 rx.await.map_err(|_| crate::Error::Shutdown)
1154 }
1155
1156 pub async fn set_super_seeding(&self, enabled: bool) -> crate::Result<()> {
1158 let (tx, rx) = oneshot::channel();
1159 self.cmd_tx
1160 .send(TorrentCommand::SetSuperSeeding { enabled, reply: tx })
1161 .await
1162 .map_err(|_| crate::Error::Shutdown)?;
1163 rx.await.map_err(|_| crate::Error::Shutdown)
1164 }
1165
1166 pub async fn is_super_seeding(&self) -> crate::Result<bool> {
1168 let (tx, rx) = oneshot::channel();
1169 self.cmd_tx
1170 .send(TorrentCommand::IsSuperSeeding { reply: tx })
1171 .await
1172 .map_err(|_| crate::Error::Shutdown)?;
1173 rx.await.map_err(|_| crate::Error::Shutdown)
1174 }
1175
1176 pub async fn set_seed_mode(&self, enabled: bool) -> crate::Result<()> {
1187 let (tx, rx) = oneshot::channel();
1188 self.cmd_tx
1189 .send(TorrentCommand::SetSeedMode { enabled, reply: tx })
1190 .await
1191 .map_err(|_| crate::Error::Shutdown)?;
1192 rx.await.map_err(|_| crate::Error::Shutdown)
1193 }
1194
1195 pub async fn add_tracker(&self, url: String) -> crate::Result<()> {
1199 self.cmd_tx
1200 .send(TorrentCommand::AddTracker { url })
1201 .await
1202 .map_err(|_| crate::Error::Shutdown)
1203 }
1204
1205 pub async fn replace_trackers(&self, urls: Vec<String>) -> crate::Result<()> {
1207 let (tx, rx) = oneshot::channel();
1208 self.cmd_tx
1209 .send(TorrentCommand::ReplaceTrackers { urls, reply: tx })
1210 .await
1211 .map_err(|_| crate::Error::Shutdown)?;
1212 rx.await.map_err(|_| crate::Error::Shutdown)
1213 }
1214
1215 pub async fn force_recheck(&self) -> crate::Result<()> {
1222 let (tx, rx) = oneshot::channel();
1223 self.cmd_tx
1224 .send(TorrentCommand::ForceRecheck { reply: tx })
1225 .await
1226 .map_err(|_| crate::Error::Shutdown)?;
1227 rx.await.map_err(|_| crate::Error::Shutdown)?
1228 }
1229
1230 pub async fn rename_file(&self, file_index: usize, new_name: String) -> crate::Result<()> {
1236 let (tx, rx) = oneshot::channel();
1237 self.cmd_tx
1238 .send(TorrentCommand::RenameFile {
1239 file_index,
1240 new_name,
1241 reply: tx,
1242 })
1243 .await
1244 .map_err(|_| crate::Error::Shutdown)?;
1245 rx.await.map_err(|_| crate::Error::Shutdown)?
1246 }
1247
1248 pub(crate) async fn spawn_ssl_peer(
1250 &self,
1251 addr: SocketAddr,
1252 stream: impl tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
1253 ) -> crate::Result<()> {
1254 self.cmd_tx
1255 .send(TorrentCommand::SpawnSslPeer {
1256 addr,
1257 stream: crate::types::BoxedAsyncStream(Box::new(stream)),
1258 })
1259 .await
1260 .map_err(|_| crate::Error::Shutdown)
1261 }
1262
1263 pub async fn set_max_connections(&self, limit: usize) -> crate::Result<()> {
1265 let (tx, rx) = oneshot::channel();
1266 self.cmd_tx
1267 .send(TorrentCommand::SetMaxConnections { limit, reply: tx })
1268 .await
1269 .map_err(|_| crate::Error::Shutdown)?;
1270 rx.await.map_err(|_| crate::Error::Shutdown)
1271 }
1272
1273 pub async fn max_connections(&self) -> crate::Result<usize> {
1275 let (tx, rx) = oneshot::channel();
1276 self.cmd_tx
1277 .send(TorrentCommand::MaxConnections { reply: tx })
1278 .await
1279 .map_err(|_| crate::Error::Shutdown)?;
1280 rx.await.map_err(|_| crate::Error::Shutdown)
1281 }
1282
1283 pub async fn set_max_uploads(&self, limit: usize) -> crate::Result<()> {
1285 let (tx, rx) = oneshot::channel();
1286 self.cmd_tx
1287 .send(TorrentCommand::SetMaxUploads { limit, reply: tx })
1288 .await
1289 .map_err(|_| crate::Error::Shutdown)?;
1290 rx.await.map_err(|_| crate::Error::Shutdown)
1291 }
1292
1293 pub async fn max_uploads(&self) -> crate::Result<usize> {
1295 let (tx, rx) = oneshot::channel();
1296 self.cmd_tx
1297 .send(TorrentCommand::MaxUploads { reply: tx })
1298 .await
1299 .map_err(|_| crate::Error::Shutdown)?;
1300 rx.await.map_err(|_| crate::Error::Shutdown)
1301 }
1302
1303 pub async fn get_peer_info(&self) -> crate::Result<Vec<PeerInfo>> {
1305 let (tx, rx) = oneshot::channel();
1306 self.cmd_tx
1307 .send(TorrentCommand::GetPeerInfo { reply: tx })
1308 .await
1309 .map_err(|_| crate::Error::Shutdown)?;
1310 rx.await.map_err(|_| crate::Error::Shutdown)
1311 }
1312
1313 pub async fn get_download_queue(&self) -> crate::Result<Vec<PartialPieceInfo>> {
1315 let (tx, rx) = oneshot::channel();
1316 self.cmd_tx
1317 .send(TorrentCommand::GetDownloadQueue { reply: tx })
1318 .await
1319 .map_err(|_| crate::Error::Shutdown)?;
1320 rx.await.map_err(|_| crate::Error::Shutdown)
1321 }
1322
1323 pub async fn have_piece(&self, index: u32) -> crate::Result<bool> {
1325 let (tx, rx) = oneshot::channel();
1326 self.cmd_tx
1327 .send(TorrentCommand::HavePiece { index, reply: tx })
1328 .await
1329 .map_err(|_| crate::Error::Shutdown)?;
1330 rx.await.map_err(|_| crate::Error::Shutdown)
1331 }
1332
1333 pub async fn piece_availability(&self) -> crate::Result<Vec<u32>> {
1335 let (tx, rx) = oneshot::channel();
1336 self.cmd_tx
1337 .send(TorrentCommand::PieceAvailability { reply: tx })
1338 .await
1339 .map_err(|_| crate::Error::Shutdown)?;
1340 rx.await.map_err(|_| crate::Error::Shutdown)
1341 }
1342
1343 pub async fn file_progress(&self) -> crate::Result<Vec<u64>> {
1345 let (tx, rx) = oneshot::channel();
1346 self.cmd_tx
1347 .send(TorrentCommand::FileProgress { reply: tx })
1348 .await
1349 .map_err(|_| crate::Error::Shutdown)?;
1350 rx.await.map_err(|_| crate::Error::Shutdown)
1351 }
1352
1353 pub async fn info_hashes(&self) -> crate::Result<irontide_core::InfoHashes> {
1355 let (tx, rx) = oneshot::channel();
1356 self.cmd_tx
1357 .send(TorrentCommand::InfoHashes { reply: tx })
1358 .await
1359 .map_err(|_| crate::Error::Shutdown)?;
1360 rx.await.map_err(|_| crate::Error::Shutdown)
1361 }
1362
1363 pub async fn torrent_file(&self) -> crate::Result<Option<TorrentMetaV1>> {
1367 let (tx, rx) = oneshot::channel();
1368 self.cmd_tx
1369 .send(TorrentCommand::TorrentFile { reply: tx })
1370 .await
1371 .map_err(|_| crate::Error::Shutdown)?;
1372 rx.await.map_err(|_| crate::Error::Shutdown)
1373 }
1374
1375 pub async fn torrent_file_v2(&self) -> crate::Result<Option<irontide_core::TorrentMetaV2>> {
1380 let (tx, rx) = oneshot::channel();
1381 self.cmd_tx
1382 .send(TorrentCommand::TorrentFileV2 { reply: tx })
1383 .await
1384 .map_err(|_| crate::Error::Shutdown)?;
1385 rx.await.map_err(|_| crate::Error::Shutdown)
1386 }
1387
1388 pub async fn force_dht_announce(&self) -> crate::Result<()> {
1392 self.cmd_tx
1393 .send(TorrentCommand::ForceDhtAnnounce)
1394 .await
1395 .map_err(|_| crate::Error::Shutdown)
1396 }
1397
1398 pub async fn read_piece(&self, index: u32) -> crate::Result<Bytes> {
1403 let (tx, rx) = oneshot::channel();
1404 self.cmd_tx
1405 .send(TorrentCommand::ReadPiece { index, reply: tx })
1406 .await
1407 .map_err(|_| crate::Error::Shutdown)?;
1408 rx.await.map_err(|_| crate::Error::Shutdown)?
1409 }
1410
1411 pub async fn flush_cache(&self) -> crate::Result<()> {
1413 let (tx, rx) = oneshot::channel();
1414 self.cmd_tx
1415 .send(TorrentCommand::FlushCache { reply: tx })
1416 .await
1417 .map_err(|_| crate::Error::Shutdown)?;
1418 rx.await.map_err(|_| crate::Error::Shutdown)?
1419 }
1420
1421 pub fn is_valid(&self) -> bool {
1426 !self.cmd_tx.is_closed()
1427 }
1428
1429 pub async fn clear_error(&self) -> crate::Result<()> {
1431 self.cmd_tx
1432 .send(TorrentCommand::ClearError)
1433 .await
1434 .map_err(|_| crate::Error::Shutdown)
1435 }
1436
1437 pub async fn file_status(&self) -> crate::Result<Vec<crate::types::FileStatus>> {
1441 let (tx, rx) = oneshot::channel();
1442 self.cmd_tx
1443 .send(TorrentCommand::FileStatus { reply: tx })
1444 .await
1445 .map_err(|_| crate::Error::Shutdown)?;
1446 rx.await.map_err(|_| crate::Error::Shutdown)
1447 }
1448
1449 pub async fn flags(&self) -> crate::Result<crate::types::TorrentFlags> {
1451 let (tx, rx) = oneshot::channel();
1452 self.cmd_tx
1453 .send(TorrentCommand::Flags { reply: tx })
1454 .await
1455 .map_err(|_| crate::Error::Shutdown)?;
1456 rx.await.map_err(|_| crate::Error::Shutdown)
1457 }
1458
1459 pub async fn set_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
1463 let (tx, rx) = oneshot::channel();
1464 self.cmd_tx
1465 .send(TorrentCommand::SetFlags { flags, reply: tx })
1466 .await
1467 .map_err(|_| crate::Error::Shutdown)?;
1468 rx.await.map_err(|_| crate::Error::Shutdown)
1469 }
1470
1471 pub async fn unset_flags(&self, flags: crate::types::TorrentFlags) -> crate::Result<()> {
1475 let (tx, rx) = oneshot::channel();
1476 self.cmd_tx
1477 .send(TorrentCommand::UnsetFlags { flags, reply: tx })
1478 .await
1479 .map_err(|_| crate::Error::Shutdown)?;
1480 rx.await.map_err(|_| crate::Error::Shutdown)
1481 }
1482
1483 pub async fn connect_peer(&self, addr: SocketAddr) -> crate::Result<()> {
1488 self.cmd_tx
1489 .send(TorrentCommand::ConnectPeer { addr })
1490 .await
1491 .map_err(|_| crate::Error::Shutdown)
1492 }
1493
1494 pub(crate) fn send_pre_resolved_metadata(&self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
1500 let _ = self
1501 .cmd_tx
1502 .try_send(TorrentCommand::PreResolvedMetadata { info_bytes, peers });
1503 }
1504}
1505
1506#[derive(Debug, Clone)]
1512pub(crate) struct CachedFileEntry {
1513 pub(crate) index: usize,
1514 #[allow(dead_code)] pub(crate) length: u64,
1516 pub(crate) first_piece: u32,
1517 pub(crate) last_piece: u32,
1518}
1519
1520#[derive(Debug, Clone)]
1522pub(crate) struct CachedFileInfo {
1523 pub(crate) entries: Vec<CachedFileEntry>,
1524}
1525
1526pub(crate) fn build_cached_file_info(meta: &TorrentMetaV1, lengths: &Lengths) -> CachedFileInfo {
1527 let piece_length = lengths.piece_length();
1528 let files = meta.info.files();
1529 let mut entries = Vec::with_capacity(files.len());
1530 let mut offset = 0u64;
1531 for (index, file) in files.iter().enumerate() {
1532 let first_piece = (offset / piece_length) as u32;
1533 let last_piece = if file.length == 0 {
1534 first_piece
1535 } else {
1536 ((offset + file.length - 1) / piece_length) as u32
1537 };
1538 entries.push(CachedFileEntry {
1539 index,
1540 length: file.length,
1541 first_piece,
1542 last_piece,
1543 });
1544 offset += file.length;
1545 }
1546 CachedFileInfo { entries }
1547}
1548
1549pub(crate) struct TorrentActor {
1554 pub(crate) config: TorrentConfig,
1555 pub(crate) lock_timing: crate::timed_lock::LockTimingSettings,
1557 pub(crate) info_hash: Id20,
1558 pub(crate) our_peer_id: Id20,
1559 pub(crate) state: TorrentState,
1560
1561 pub(crate) disk: Option<DiskHandle>,
1563 pub(crate) disk_manager: DiskManagerHandle,
1564 pub(crate) chunk_tracker: Option<ChunkTracker>,
1565 pub(crate) lengths: Option<Lengths>,
1566 pub(crate) num_pieces: u32,
1567
1568 pub(crate) file_priorities: Vec<FilePriority>,
1570 pub(crate) wanted_pieces: Bitfield,
1571 pub(crate) end_game: EndGame,
1572
1573 pub(crate) streaming_pieces: BTreeSet<u32>,
1575 pub(crate) time_critical_pieces: BTreeSet<u32>,
1576 pub(crate) streaming_cursors: Vec<crate::streaming::StreamingCursor>,
1577 pub(crate) piece_ready_tx: broadcast::Sender<u32>,
1578 pub(crate) have_watch_tx: tokio::sync::watch::Sender<Bitfield>,
1579 pub(crate) have_watch_rx: tokio::sync::watch::Receiver<Bitfield>,
1580 pub(crate) stream_read_semaphore: Arc<tokio::sync::Semaphore>,
1581
1582 pub(crate) peers: HashMap<SocketAddr, PeerState>,
1584 pub(crate) cached_peer_rates: FxHashMap<SocketAddr, f64>,
1587 #[allow(dead_code)]
1589 pub(crate) refill_notify: Arc<tokio::sync::Notify>,
1590 pub(crate) atomic_states: Option<Arc<crate::piece_reservation::AtomicPieceStates>>,
1592 pub(crate) block_maps: Option<Arc<BlockMaps>>,
1594 pub(crate) steal_candidates: Option<Arc<StealCandidates>>,
1596 pub(crate) last_steal_populate: Instant,
1598 pub(crate) piece_write_guards: Option<Arc<crate::piece_reservation::PieceWriteGuards>>,
1600 pub(crate) snapshot_dirty: bool,
1602 pub(crate) availability_snapshot: Option<Arc<crate::piece_reservation::AvailabilitySnapshot>>,
1604 pub(crate) snapshot_generation: u64,
1606 pub(crate) piece_owner: Vec<Option<u16>>,
1608 pub(crate) peer_slab: crate::piece_reservation::PeerSlab,
1610 pub(crate) availability: Vec<u32>,
1612 pub(crate) priority_pieces: BTreeSet<u32>,
1614 pub(crate) max_in_flight: usize,
1616 pub(crate) reservation_notify: Option<Arc<tokio::sync::Notify>>,
1618 pub(crate) choker: Choker,
1619 pub(crate) user_seed_mode: bool,
1626 pub(crate) max_connections: usize,
1628 pub(crate) peer_states: Option<Arc<crate::peer_states::PeerStates>>,
1630 pub(crate) connect_semaphore: Arc<tokio::sync::Semaphore>,
1633 pub(crate) connect_permits:
1636 HashMap<SocketAddr, Arc<parking_lot::Mutex<Option<tokio::sync::OwnedSemaphorePermit>>>>,
1637 pub(crate) connect_rx: Option<mpsc::Receiver<ConnectPeer>>,
1639
1640 pub(crate) metadata_downloader: Option<MetadataDownloader>,
1642
1643 pub(crate) meta: Option<TorrentMetaV1>,
1645
1646 pub(crate) cached_files: Option<CachedFileInfo>,
1648
1649 pub(crate) downloaded: u64,
1651 pub(crate) uploaded: u64,
1652 pub(crate) checking_progress: f32,
1653 pub(crate) total_download: u64,
1654 pub(crate) total_upload: u64,
1655 pub(crate) total_failed_bytes: u64,
1656 pub(crate) total_redundant_bytes: u64,
1657 pub(crate) added_time: i64,
1658 pub(crate) completed_time: i64,
1659 pub(crate) last_download: i64,
1660 pub(crate) last_upload: i64,
1661 pub(crate) last_seen_complete: i64,
1662 pub(crate) active_duration: i64,
1663 pub(crate) finished_duration: i64,
1664 pub(crate) seeding_duration: i64,
1665 pub(crate) active_since: Option<std::time::Instant>,
1666 pub(crate) state_duration_since: Option<std::time::Instant>,
1667 #[allow(dead_code)] pub(crate) started_at: std::time::Instant,
1669 pub(crate) moving_storage: bool,
1670 pub(crate) has_incoming: bool,
1671 pub(crate) need_save_resume: bool,
1672 pub(crate) error: String,
1673 pub(crate) error_file: i32,
1674
1675 pub(crate) cmd_rx: mpsc::Receiver<TorrentCommand>,
1677 pub(crate) event_tx: mpsc::Sender<PeerEvent>,
1678 pub(crate) event_rx: mpsc::Receiver<PeerEvent>,
1679
1680 pub(crate) write_error_rx: mpsc::Receiver<crate::disk::DiskWriteError>,
1682 pub(crate) write_error_tx: mpsc::Sender<crate::disk::DiskWriteError>,
1683 pub(crate) verify_result_rx: mpsc::Receiver<crate::disk::VerifyResult>,
1684 pub(crate) verify_result_tx: mpsc::Sender<crate::disk::VerifyResult>,
1685 pub(crate) pending_verify: HashSet<u32>,
1688 pub(crate) piece_generations: Vec<u64>,
1691 pub(crate) hash_result_rx: tokio::sync::mpsc::Receiver<crate::hash_pool::HashResult>,
1693 pub(crate) hash_result_tx: tokio::sync::mpsc::Sender<crate::hash_pool::HashResult>,
1695
1696 pub(crate) listener: Option<Box<dyn crate::transport::TransportListener>>,
1698
1699 pub(crate) utp_socket: Option<irontide_utp::UtpSocket>,
1701 pub(crate) utp_socket_v6: Option<irontide_utp::UtpSocket>,
1703
1704 pub(crate) tracker_manager: TrackerManager,
1706 pub(crate) tracker_result_rx: Option<mpsc::Receiver<crate::tracker_manager::TrackerPeerBatch>>,
1709
1710 pub(crate) dht: Option<DhtHandle>,
1712 pub(crate) dht_v6: Option<DhtHandle>,
1713 pub(crate) dht_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
1714 pub(crate) dht_v6_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
1715 pub(crate) dht_v6_empty_count: u32,
1718 pub(crate) dht_v6_last_retry: Option<std::time::Instant>,
1720
1721 pub(crate) alert_tx: broadcast::Sender<Alert>,
1723 pub(crate) alert_mask: Arc<AtomicU32>,
1724
1725 pub(crate) upload_bucket: crate::rate_limiter::TokenBucket,
1727 pub(crate) download_bucket: crate::rate_limiter::TokenBucket,
1728 pub(crate) global_upload_bucket: Option<SharedBucket>,
1729 #[allow(dead_code)] pub(crate) global_download_bucket: Option<SharedBucket>,
1731 pub(crate) slot_tuner: crate::slot_tuner::SlotTuner,
1732 pub(crate) upload_bytes_interval: u64,
1733
1734 pub(crate) peak_download_rate: u64,
1736
1737 pub(crate) web_seeds: HashMap<String, mpsc::Sender<crate::web_seed::WebSeedCommand>>,
1739 pub(crate) banned_web_seeds: HashSet<String>,
1740 pub(crate) web_seed_in_flight: HashMap<u32, String>,
1741
1742 pub(crate) super_seed: Option<crate::super_seed::SuperSeedState>,
1744 pub(crate) have_broadcast_tx: tokio::sync::broadcast::Sender<u32>,
1746
1747 pub(crate) suggested_to_peers: HashMap<SocketAddr, HashSet<u32>>,
1749
1750 pub(crate) predictive_have_sent: HashSet<u32>,
1752
1753 pub(crate) ban_manager: crate::session::SharedBanManager,
1755 pub(crate) piece_contributors: HashMap<u32, HashSet<std::net::IpAddr>>,
1756 pub(crate) parole_pieces: HashMap<u32, crate::ban::ParoleState>,
1757
1758 pub(crate) ip_filter: crate::session::SharedIpFilter,
1760
1761 pub(crate) external_ip: Option<std::net::IpAddr>,
1763
1764 pub(crate) share_lru: std::collections::VecDeque<u32>,
1768 pub(crate) share_max_pieces: usize,
1770
1771 pub(crate) plugins: Arc<Vec<Box<dyn crate::extension::ExtensionPlugin>>>,
1773
1774 pub(crate) hash_picker: Option<irontide_core::HashPicker>,
1776 pub(crate) version: irontide_core::TorrentVersion,
1777 #[allow(dead_code)] pub(crate) meta_v2: Option<irontide_core::TorrentMetaV2>,
1779
1780 pub(crate) info_hashes: irontide_core::InfoHashes,
1782
1783 pub(crate) dht_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
1785 pub(crate) dht_v6_v2_peers_rx: Option<mpsc::UnboundedReceiver<Vec<SocketAddr>>>,
1786
1787 pub(crate) magnet_selected_files: Option<Vec<irontide_core::FileSelection>>,
1790
1791 pub(crate) sam_session: Option<Arc<crate::i2p::SamSession>>,
1793
1794 pub(crate) i2p_accept_rx: Option<mpsc::Receiver<crate::i2p::SamStream>>,
1796
1797 pub(crate) i2p_peer_counter: u32,
1799
1800 pub(crate) i2p_destinations: HashMap<SocketAddr, crate::i2p::I2pDestination>,
1802
1803 pub(crate) ssl_manager: Option<Arc<crate::ssl_manager::SslManager>>,
1805
1806 pub(crate) rate_limiter_set: crate::rate_limiter::RateLimiterSet,
1808 pub(crate) auto_sequential_active: bool,
1810 pub(crate) factory: Arc<crate::transport::NetworkFactory>,
1812 pub(crate) hash_pool_ref: Option<std::sync::Arc<crate::hash_pool::HashPool>>,
1814 pub(crate) live_outgoing_peers:
1816 std::sync::Arc<parking_lot::RwLock<std::collections::HashSet<SocketAddr>>>,
1817 pub(crate) connect_attempts: u64,
1819 pub(crate) connect_failures: u64,
1821 pub(crate) choke_rotations: u64,
1823 pub(crate) inflight_started: Vec<Option<Instant>>,
1826 pub(crate) completed_piece_times: std::collections::VecDeque<Duration>,
1828 pub(crate) piece_steals: u64,
1830 pub(crate) holepunch_cooldowns: HashMap<SocketAddr, Instant>,
1832 pub(crate) holepunch_pending: Vec<SocketAddr>,
1834}
1835
1836pub(crate) const END_GAME_DEPTH: usize = 128;
1843
1844impl TorrentActor {
1847 async fn run(mut self) {
1849 self.verify_existing_pieces().await;
1851
1852 if let Some(ct) = &self.chunk_tracker {
1855 let atomic_states = Arc::new(AtomicPieceStates::new(
1856 self.num_pieces,
1857 ct.bitfield(),
1858 &self.wanted_pieces,
1859 ));
1860 self.atomic_states = Some(Arc::clone(&atomic_states));
1861 self.availability = vec![0u32; self.num_pieces as usize];
1862 self.piece_owner = vec![None; self.num_pieces as usize];
1863 self.inflight_started = vec![None; self.num_pieces as usize];
1865 self.max_in_flight = self.config.max_in_flight_pieces;
1866
1867 if self.config.use_block_stealing {
1869 if let Some(ref lengths) = self.lengths {
1870 self.block_maps = Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
1871 }
1872 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
1873 }
1874 self.piece_write_guards = Some(Arc::new(
1876 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
1877 ));
1878
1879 let snapshot = Arc::new(AvailabilitySnapshot::build(
1880 &self.availability,
1881 &atomic_states,
1882 &self.priority_pieces,
1883 0,
1884 ));
1885 self.availability_snapshot = Some(snapshot);
1886 self.snapshot_generation = 0;
1887
1888 let notify = Arc::new(tokio::sync::Notify::new());
1889 self.reservation_notify = Some(notify);
1890 }
1891
1892 if self.state != TorrentState::Seeding {
1894 self.spawn_web_seeds();
1895 self.assign_pieces_to_web_seeds();
1896 }
1897
1898 let connect_semaphore = Arc::new(tokio::sync::Semaphore::new(
1901 self.effective_max_connections(),
1902 ));
1903 self.connect_semaphore = Arc::clone(&connect_semaphore);
1904 self.connect_permits.clear();
1905 let (queue_tx, queue_rx) = mpsc::unbounded_channel();
1907 let peer_states = Arc::new(crate::peer_states::PeerStates::new(queue_tx));
1908 self.peer_states = Some(Arc::clone(&peer_states));
1909 let (adder_connect_tx, adder_connect_rx) = mpsc::channel(64);
1910 self.connect_rx = Some(adder_connect_rx);
1911 tokio::spawn(peer_adder::peer_adder_task(
1913 queue_rx,
1914 Arc::clone(&connect_semaphore),
1915 Arc::clone(&peer_states),
1916 Arc::clone(&self.ban_manager),
1917 Arc::clone(&self.ip_filter),
1918 adder_connect_tx,
1919 ));
1920
1921 let mut unchoke_interval = tokio::time::interval(Duration::from_secs(10));
1922 let mut optimistic_interval = tokio::time::interval(Duration::from_secs(30));
1923 let mut refill_interval = tokio::time::interval(Duration::from_millis(100));
1924 let mut dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::ZERO));
1925 let mut suggest_interval = if self.config.suggest_mode {
1926 Some(tokio::time::interval(Duration::from_secs(30)))
1927 } else {
1928 None
1929 };
1930 let mut turnover_interval = tokio::time::interval(Duration::from_secs(1));
1932 let mut pipeline_tick_interval = tokio::time::interval(Duration::from_millis(1000));
1933 pipeline_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
1935 let mut end_game_tick_interval = tokio::time::interval(Duration::from_millis(200));
1936 end_game_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
1937 let mut diag_interval = tokio::time::interval(Duration::from_secs(5));
1938 let mut conn_stats_interval = tokio::time::interval(Duration::from_secs(30));
1940 let mut metadata_timeout_interval = tokio::time::interval(Duration::from_secs(5));
1942 let mut snapshot_rebuild_interval = tokio::time::interval(Duration::from_millis(50));
1944 let mut soft_reap_interval = tokio::time::interval(Duration::from_secs(1));
1946 let mut eviction_interval = tokio::time::interval(Duration::from_secs(2));
1949 eviction_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
1950
1951 unchoke_interval.tick().await;
1953 optimistic_interval.tick().await;
1954 refill_interval.tick().await;
1955 if let Some(ref mut si) = suggest_interval {
1957 si.tick().await; }
1959 turnover_interval.tick().await;
1960 pipeline_tick_interval.tick().await;
1961 end_game_tick_interval.tick().await;
1962 diag_interval.tick().await;
1963 conn_stats_interval.tick().await;
1964 metadata_timeout_interval.tick().await;
1965 snapshot_rebuild_interval.tick().await;
1966 soft_reap_interval.tick().await;
1967 eviction_interval.tick().await;
1968
1969 if self.state == TorrentState::Downloading && self.config.enable_dht {
1972 if let Some(ref dht) = self.dht
1974 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
1975 {
1976 warn!("DHT v4 announce failed: {e}");
1977 }
1978 if let Some(ref dht6) = self.dht_v6
1979 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
1980 {
1981 debug!("DHT v6 announce failed: {e}");
1982 }
1983 if self.info_hashes.is_hybrid()
1985 && let Some(v2) = self.info_hashes.v2
1986 {
1987 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
1988 if v2_as_v1 != self.info_hash {
1989 if let Some(ref dht) = self.dht
1990 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
1991 {
1992 debug!("DHT v4 dual-swarm announce failed: {e}");
1993 }
1994 if let Some(ref dht6) = self.dht_v6
1995 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
1996 {
1997 debug!("DHT v6 dual-swarm announce failed: {e}");
1998 }
1999 }
2000 }
2001 }
2002
2003 if self.config.enable_i2p
2006 && let Some(ref sam) = self.sam_session
2007 {
2008 let (tx, rx) = mpsc::channel(16);
2009 let sam = Arc::clone(sam);
2010 tokio::spawn(async move {
2011 loop {
2012 match sam.accept().await {
2013 Ok(stream) => {
2014 if tx.send(stream).await.is_err() {
2015 break; }
2017 }
2018 Err(e) => {
2019 warn!("I2P accept error: {e}");
2020 tokio::time::sleep(Duration::from_secs(5)).await;
2021 }
2022 }
2023 }
2024 });
2025 self.i2p_accept_rx = Some(rx);
2026 }
2027
2028 loop {
2029 tokio::select! {
2030 biased;
2031 event = self.event_rx.recv() => {
2036 if let Some(event) = event {
2037 self.handle_peer_event(event).await;
2038 for _ in 0..512 {
2040 match self.event_rx.try_recv() {
2041 Ok(event) => self.handle_peer_event(event).await,
2042 Err(_) => break,
2043 }
2044 }
2045 }
2046 }
2047 Some(result) = self.verify_result_rx.recv() => {
2049 self.pending_verify.remove(&result.piece);
2050 let dominated = self.chunk_tracker.as_ref()
2052 .map(|ct| ct.bitfield().get(result.piece))
2053 .unwrap_or(false);
2054 if !dominated {
2055 if result.passed {
2056 self.on_piece_verified(result.piece).await;
2057 } else {
2058 self.on_piece_hash_failed(result.piece).await;
2059 }
2061 }
2062 }
2063 Some(result) = self.hash_result_rx.recv() => {
2065 self.handle_hash_result(result).await;
2066 }
2067 cmd = self.cmd_rx.recv() => {
2069 match cmd {
2070 Some(TorrentCommand::AddPeers { peers, source }) => {
2071 self.handle_add_peers(peers, source);
2072 }
2073 Some(TorrentCommand::Stats { reply }) => {
2074 let _ = reply.send(self.make_stats());
2075 }
2076 Some(TorrentCommand::Pause) => {
2077 self.handle_pause().await;
2078 }
2079 Some(TorrentCommand::Resume) => {
2080 self.handle_resume().await;
2081 }
2082 Some(TorrentCommand::SaveResumeData { reply }) => {
2083 let result = self.build_resume_data();
2084 let _ = reply.send(result);
2085 }
2086 Some(TorrentCommand::SetFilePriority { index, priority, reply }) => {
2087 let result = self.handle_set_file_priority(index, priority);
2088 let _ = reply.send(result);
2089 }
2090 Some(TorrentCommand::FilePriorities { reply }) => {
2091 let _ = reply.send(self.file_priorities.clone());
2092 }
2093 Some(TorrentCommand::ForceReannounce) => {
2094 self.tracker_manager.force_reannounce();
2095 }
2096 Some(TorrentCommand::TrackerList { reply }) => {
2097 let _ = reply.send(self.tracker_manager.tracker_list());
2098 }
2099 Some(TorrentCommand::Scrape { reply }) => {
2100 let result = self.tracker_manager.scrape().await;
2101 if let Some((ref url, ref info)) = result {
2102 post_alert(&self.alert_tx, &self.alert_mask, AlertKind::ScrapeReply {
2103 info_hash: self.info_hash,
2104 url: url.clone(),
2105 complete: info.complete,
2106 incomplete: info.incomplete,
2107 downloaded: info.downloaded,
2108 });
2109 }
2110 let _ = reply.send(result);
2111 }
2112 Some(TorrentCommand::OpenFile { file_index, reply }) => {
2113 let result = self.handle_open_file(file_index);
2114 let _ = reply.send(result);
2115 }
2116 Some(TorrentCommand::IncomingPeer { stream, addr }) => {
2117 self.spawn_peer_from_stream_with_mode(
2118 addr,
2119 stream,
2120 Some(irontide_wire::mse::EncryptionMode::Disabled),
2121 );
2122 }
2123 Some(TorrentCommand::UpdateExternalIp { ip }) => {
2124 self.external_ip = Some(ip);
2125 post_alert(
2126 &self.alert_tx,
2127 &self.alert_mask,
2128 AlertKind::ExternalIpDetected { ip },
2129 );
2130 }
2131 Some(TorrentCommand::MoveStorage { new_path, reply }) => {
2132 let result = self.handle_move_storage(new_path).await;
2133 let _ = reply.send(result);
2134 }
2135 Some(TorrentCommand::SpawnSslPeer { addr, stream }) => {
2136 self.spawn_peer_from_stream_with_mode(
2138 addr,
2139 stream.0,
2140 Some(irontide_wire::mse::EncryptionMode::Disabled),
2141 );
2142 }
2143 Some(TorrentCommand::SetDownloadLimit { bytes_per_sec, reply }) => {
2144 self.download_bucket.set_rate(bytes_per_sec);
2145 let _ = reply.send(());
2146 }
2147 Some(TorrentCommand::SetUploadLimit { bytes_per_sec, reply }) => {
2148 self.upload_bucket.set_rate(bytes_per_sec);
2149 let _ = reply.send(());
2150 }
2151 Some(TorrentCommand::DownloadLimit { reply }) => {
2152 let _ = reply.send(self.download_bucket.rate());
2153 }
2154 Some(TorrentCommand::UploadLimit { reply }) => {
2155 let _ = reply.send(self.upload_bucket.rate());
2156 }
2157 Some(TorrentCommand::SetSequentialDownload { enabled, reply }) => {
2158 self.config.sequential_download = enabled;
2159 let _ = reply.send(());
2160 }
2161 Some(TorrentCommand::IsSequentialDownload { reply }) => {
2162 let _ = reply.send(self.config.sequential_download);
2163 }
2164 Some(TorrentCommand::SetSuperSeeding { enabled, reply }) => {
2165 self.config.super_seeding = enabled;
2166 self.super_seed = if enabled {
2167 Some(crate::super_seed::SuperSeedState::new())
2168 } else {
2169 None
2170 };
2171 let _ = reply.send(());
2172 }
2173 Some(TorrentCommand::IsSuperSeeding { reply }) => {
2174 let _ = reply.send(self.config.super_seeding);
2175 }
2176 Some(TorrentCommand::SetSeedMode { enabled, reply }) => {
2177 self.handle_set_seed_mode(enabled);
2178 let _ = reply.send(());
2179 }
2180 Some(TorrentCommand::AddTracker { url }) => {
2181 self.tracker_manager.add_tracker_url(&url);
2182 }
2183 Some(TorrentCommand::ReplaceTrackers { urls, reply }) => {
2184 self.tracker_manager.replace_all(urls);
2185 let _ = reply.send(());
2186 }
2187 Some(TorrentCommand::ForceRecheck { reply }) => {
2188 self.handle_force_recheck(reply).await;
2189 }
2190 Some(TorrentCommand::RenameFile { file_index, new_name, reply }) => {
2191 let result = self.handle_rename_file(file_index, new_name).await;
2192 let _ = reply.send(result);
2193 }
2194 Some(TorrentCommand::SetMaxConnections { limit, reply }) => {
2195 self.max_connections = limit;
2196 let _ = reply.send(());
2197 }
2198 Some(TorrentCommand::MaxConnections { reply }) => {
2199 let _ = reply.send(self.max_connections);
2200 }
2201 Some(TorrentCommand::SetMaxUploads { limit, reply }) => {
2202 self.choker.set_unchoke_slots(limit);
2203 let _ = reply.send(());
2204 }
2205 Some(TorrentCommand::MaxUploads { reply }) => {
2206 let _ = reply.send(self.choker.unchoke_slots());
2207 }
2208 Some(TorrentCommand::GetPeerInfo { reply }) => {
2209 let _ = reply.send(self.build_peer_info());
2210 }
2211 Some(TorrentCommand::GetDownloadQueue { reply }) => {
2212 let _ = reply.send(self.build_download_queue());
2213 }
2214 Some(TorrentCommand::HavePiece { index, reply }) => {
2215 let has = self.chunk_tracker.as_ref()
2216 .map(|ct| ct.has_piece(index))
2217 .unwrap_or(false);
2218 let _ = reply.send(has);
2219 }
2220 Some(TorrentCommand::PieceAvailability { reply }) => {
2221 let avail = self.availability.clone();
2222 let _ = reply.send(avail);
2223 }
2224 Some(TorrentCommand::FileProgress { reply }) => {
2225 let _ = reply.send(self.compute_file_progress());
2226 }
2227 Some(TorrentCommand::InfoHashes { reply }) => {
2228 let _ = reply.send(self.info_hashes.clone());
2229 }
2230 Some(TorrentCommand::TorrentFile { reply }) => {
2231 let _ = reply.send(self.meta.clone());
2232 }
2233 Some(TorrentCommand::TorrentFileV2 { reply }) => {
2234 let _ = reply.send(self.meta_v2.clone());
2235 }
2236 Some(TorrentCommand::ForceDhtAnnounce) => {
2237 self.handle_force_dht_announce().await;
2238 }
2239 Some(TorrentCommand::ReadPiece { index, reply }) => {
2240 let result = self.handle_read_piece(index).await;
2241 let _ = reply.send(result);
2242 }
2243 Some(TorrentCommand::FlushCache { reply }) => {
2244 let result = self.handle_flush_cache().await;
2245 let _ = reply.send(result);
2246 }
2247 Some(TorrentCommand::ClearError) => {
2248 self.handle_clear_error().await;
2249 }
2250 Some(TorrentCommand::ClearSaveResumeFlag) => {
2251 self.need_save_resume = false;
2252 }
2253 Some(TorrentCommand::RestoreResumeBitmap { pieces, reply }) => {
2254 let result = self.handle_restore_resume_bitmap(pieces);
2255 let _ = reply.send(result);
2256 }
2257 Some(TorrentCommand::FileStatus { reply }) => {
2258 let _ = reply.send(self.build_file_status());
2259 }
2260 Some(TorrentCommand::Flags { reply }) => {
2261 let _ = reply.send(self.build_flags());
2262 }
2263 Some(TorrentCommand::SetFlags { flags, reply }) => {
2264 self.apply_set_flags(flags).await;
2265 let _ = reply.send(());
2266 }
2267 Some(TorrentCommand::UnsetFlags { flags, reply }) => {
2268 self.apply_unset_flags(flags).await;
2269 let _ = reply.send(());
2270 }
2271 Some(TorrentCommand::ConnectPeer { addr }) => {
2272 self.handle_connect_peer(addr);
2273 }
2274 Some(TorrentCommand::PreResolvedMetadata { info_bytes, peers }) => {
2275 self.handle_pre_resolved_metadata(info_bytes, peers).await;
2276 }
2277 Some(TorrentCommand::Shutdown) => {
2278 info!("torrent actor: received Shutdown command, exiting");
2279 self.shutdown_web_seeds().await;
2280 self.shutdown_peers().await;
2281 return;
2282 }
2283 None => {
2284 warn!("torrent actor: cmd_rx channel closed (all senders dropped), exiting");
2285 self.shutdown_web_seeds().await;
2286 self.shutdown_peers().await;
2287 return;
2288 }
2289 }
2290 }
2291 Some(err) = self.write_error_rx.recv() => {
2293 warn!(piece = err.piece, begin = err.begin, "async disk write failed: {}", err.error);
2294 }
2295 result = accept_incoming(&mut self.listener) => {
2297 if let Ok((stream, addr)) = result {
2298 self.spawn_peer_from_stream(addr, stream);
2299 }
2300 }
2301 stream = accept_i2p(&mut self.i2p_accept_rx) => {
2303 if let Some(stream) = stream {
2304 self.handle_i2p_incoming(stream);
2305 }
2306 }
2307 _ = unchoke_interval.tick() => {
2309 self.update_peer_rates();
2310 if self.state == TorrentState::Seeding
2317 || self.state == TorrentState::Sharing
2318 {
2319 self.slot_tuner.observe(self.upload_bytes_interval);
2320 self.choker.observe_throughput(self.upload_bytes_interval);
2321 self.upload_bytes_interval = 0;
2322 self.choker.set_unchoke_slots(self.slot_tuner.current_slots());
2323 self.run_choker().await;
2324 } else {
2325 self.upload_bytes_interval = 0;
2326 }
2327 self.update_streaming_cursors();
2329 if self.config.auto_sequential {
2331 self.auto_sequential_active = crate::piece_selector::evaluate_auto_sequential(
2332 self.piece_owner.iter().filter(|o| o.is_some()).count(),
2333 self.peers.len(),
2334 self.auto_sequential_active,
2335 );
2336 }
2337 self.assign_pieces_to_web_seeds();
2339 }
2340 _ = optimistic_interval.tick() => {
2342 self.rotate_optimistic();
2343 }
2344 Some(connect_peer) = async {
2346 match self.connect_rx.as_mut() {
2347 Some(rx) => rx.recv().await,
2348 None => std::future::pending().await,
2349 }
2350 } => {
2351 self.handle_adder_connect(connect_peer);
2352 }
2353 () = &mut dht_requery_sleep, if self.state != TorrentState::Complete
2354 && self.state != TorrentState::Paused
2355 && self.state != TorrentState::Seeding
2356 && self.state != TorrentState::Stopped => {
2357 self.run_dht_requery().await;
2358 dht_requery_sleep = Box::pin(tokio::time::sleep(Duration::from_secs(60)));
2359 }
2360 _ = async {
2365 match self.tracker_manager.next_announce_in() {
2366 Some(dur) => tokio::time::sleep(dur).await,
2367 None => std::future::pending().await,
2368 }
2369 }, if self.tracker_result_rx.is_none() => {
2370 let left = self.calculate_left();
2371 self.tracker_result_rx = Some(self.tracker_manager.start_announce(
2372 irontide_tracker::AnnounceEvent::None,
2373 self.uploaded,
2374 self.downloaded,
2375 left,
2376 ));
2377 }
2378 result = async {
2381 match self.tracker_result_rx.as_mut() {
2382 Some(rx) => rx.recv().await,
2383 None => std::future::pending().await,
2384 }
2385 } => {
2386 match result {
2387 Some(batch) => {
2388 let (peers, outcome) = self.tracker_manager.process_tracker_result(batch);
2389 self.fire_tracker_alerts(&[outcome]);
2390 if !peers.is_empty() {
2391 debug!(count = peers.len(), "tracker returned peers (streaming)");
2392 self.handle_add_peers(peers, PeerSource::Tracker);
2393 }
2394 }
2395 None => {
2396 self.tracker_result_rx = None;
2399 }
2400 }
2401 }
2402 result = async {
2404 match &mut self.dht_peers_rx {
2405 Some(rx) => rx.recv().await,
2406 None => std::future::pending().await,
2407 }
2408 } => {
2409 match result {
2410 Some(peers) => {
2411 debug!(count = peers.len(), "DHT v4 returned peers");
2412 self.handle_add_peers(peers, PeerSource::Dht);
2413 }
2414 None => {
2415 debug!("DHT v4 peer search exhausted");
2416 self.dht_peers_rx = None;
2417 }
2418 }
2419 }
2420 result = async {
2422 match &mut self.dht_v6_peers_rx {
2423 Some(rx) => rx.recv().await,
2424 None => std::future::pending().await,
2425 }
2426 } => {
2427 match result {
2428 Some(peers) => {
2429 debug!(count = peers.len(), "DHT v6 returned peers");
2430 self.dht_v6_empty_count = 0; self.handle_add_peers(peers, PeerSource::Dht);
2432 }
2433 None => {
2434 self.dht_v6_peers_rx = None;
2435 self.dht_v6_empty_count += 1;
2436 if self.dht_v6_empty_count == 30 {
2437 debug!("DHT v6 routing table persistently empty, giving up");
2438 } else if self.dht_v6_empty_count < 30 {
2439 debug!("DHT v6 peer search exhausted");
2440 }
2441 }
2442 }
2443 }
2444 result = async {
2446 match &mut self.dht_v2_peers_rx {
2447 Some(rx) => rx.recv().await,
2448 None => std::future::pending().await,
2449 }
2450 } => {
2451 match result {
2452 Some(peers) => {
2453 debug!(count = peers.len(), "DHT v4 v2-swarm returned peers");
2454 self.handle_add_peers(peers, PeerSource::Dht);
2455 }
2456 None => {
2457 debug!("DHT v4 v2-swarm peer search exhausted");
2458 self.dht_v2_peers_rx = None;
2459 }
2460 }
2461 }
2462 result = async {
2464 match &mut self.dht_v6_v2_peers_rx {
2465 Some(rx) => rx.recv().await,
2466 None => std::future::pending().await,
2467 }
2468 } => {
2469 match result {
2470 Some(peers) => {
2471 debug!(count = peers.len(), "DHT v6 v2-swarm returned peers");
2472 self.handle_add_peers(peers, PeerSource::Dht);
2473 }
2474 None => {
2475 debug!("DHT v6 v2-swarm peer search exhausted");
2476 self.dht_v6_v2_peers_rx = None;
2477 }
2478 }
2479 }
2480 _ = async {
2482 match suggest_interval {
2483 Some(ref mut interval) => interval.tick().await,
2484 None => std::future::pending().await,
2485 }
2486 } => {
2487 self.suggest_cached_pieces().await;
2488 }
2489 _ = turnover_interval.tick() => {
2490 self.run_steal_queue_maintenance();
2491 }
2492 _ = pipeline_tick_interval.tick() => {
2494 let snub_timeout = Duration::from_secs(self.config.snub_timeout_secs as u64);
2495
2496 for (_addr, peer) in self.peers.iter_mut() {
2497 peer.pipeline.tick();
2498
2499 if !peer.peer_choking && !peer.snubbed {
2501 let idle = peer.last_data_received
2502 .map(|t| t.elapsed() > snub_timeout)
2503 .unwrap_or(false);
2504 if idle {
2505 peer.snubbed = true;
2506 peer.blocks_timed_out = peer.blocks_timed_out
2508 .saturating_add(peer.pending_requests.len() as u64);
2509 debug!(%_addr, "peer snubbed (no data for {}s)", self.config.snub_timeout_secs);
2510 }
2511 }
2512 }
2513
2514 self.refresh_peer_rates();
2517
2518 if !self.end_game.is_active() {
2520 self.check_end_game_activation();
2521 }
2522
2523 if let Some(ref notify) = self.reservation_notify {
2526 notify.notify_waiters();
2527 }
2528
2529 if self.config.choke_rotation_max_evictions > 0
2531 && self.state == TorrentState::Downloading
2532 {
2533 self.run_choke_rotation();
2534 }
2535 }
2536 _ = end_game_tick_interval.tick(), if self.end_game.is_active() => {
2541 let addrs: Vec<SocketAddr> = self.peers.iter()
2542 .filter(|(_, p)| !p.peer_choking && p.pending_requests.len() < END_GAME_DEPTH)
2543 .map(|(addr, _)| *addr)
2544 .collect();
2545 for addr in addrs {
2546 self.request_end_game_block(addr).await;
2547 }
2548 }
2549 _ = metadata_timeout_interval.tick(), if self.state == TorrentState::FetchingMetadata => {
2552 let timed_out: Vec<u32> = self
2554 .metadata_downloader
2555 .as_ref()
2556 .map(|dl| dl.timed_out_pieces(Duration::from_secs(5)))
2557 .unwrap_or_default();
2558
2559 if !timed_out.is_empty() {
2560 debug!(count = timed_out.len(), "metadata pieces timed out, re-requesting");
2561
2562 let eligible_senders: Vec<mpsc::Sender<PeerCommand>> = self
2565 .peers
2566 .iter()
2567 .filter(|(addr, peer)| {
2568 self.metadata_downloader
2569 .as_ref()
2570 .is_some_and(|dl| !dl.is_rejected(addr))
2571 && peer
2572 .ext_handshake
2573 .as_ref()
2574 .is_some_and(|h| h.metadata_size.is_some())
2575 })
2576 .map(|(_, peer)| peer.cmd_tx.clone())
2577 .collect();
2578
2579 for cmd_tx in &eligible_senders {
2581 for &piece in &timed_out {
2582 let _ = cmd_tx.try_send(PeerCommand::RequestMetadata { piece });
2583 }
2584 }
2585
2586 if let Some(ref mut dl) = self.metadata_downloader {
2588 for piece in timed_out {
2589 dl.reset_request_time(piece);
2590 }
2591 }
2592 }
2593 }
2594 _ = diag_interval.tick() => {
2596 {
2598 let have = self.chunk_tracker.as_ref().map(|ct| ct.bitfield().count_ones()).unwrap_or(0);
2599 let eg = self.end_game.is_active();
2600 let eg_blocks = self.end_game.block_count();
2601 info!(state = ?self.state, have, total = self.num_pieces, end_game = eg, eg_blocks, "heartbeat");
2602 }
2603 if self.state == TorrentState::Downloading {
2604 let have = self.chunk_tracker.as_ref().map(|ct| ct.bitfield().count_ones()).unwrap_or(0);
2605 let in_flight = self.atomic_states.as_ref().map(|s| s.in_flight_count() as usize).unwrap_or(0);
2606 let unchoked = self.peers.values().filter(|p| !p.peer_choking).count();
2607 info!(have, in_flight, total = self.num_pieces,
2608 downloaded_mb = self.downloaded / (1024 * 1024),
2609 peers = self.peers.len(), unchoked,
2610 "download progress");
2611 for (addr, p) in &self.peers {
2612 let last_data = p.last_data_received.map(|t| t.elapsed().as_secs()).unwrap_or(9999);
2613 trace!(%addr,
2614 choking = p.peer_choking,
2615 pending = p.pending_requests.len(),
2616 ewma_rate = p.pipeline.ewma_rate() as u64,
2617 last_data_secs = last_data,
2618 bf_ones = p.bitfield.count_ones(),
2619 "peer state");
2620 }
2621 }
2622 }
2623 _ = conn_stats_interval.tick() => {
2625 if self.connect_attempts > 0 {
2626 let succeeded = self.connect_attempts.saturating_sub(self.connect_failures);
2627 let success_pct = (succeeded as f64 / self.connect_attempts as f64 * 100.0) as u32;
2628 info!(
2629 connected = self.peers.len(),
2630 attempted = self.connect_attempts,
2631 failed = self.connect_failures,
2632 success_rate = %format!("{success_pct}%"),
2633 "connection stats"
2634 );
2635 }
2636 }
2637 _ = snapshot_rebuild_interval.tick() => {
2639 if self.snapshot_dirty {
2640 self.snapshot_dirty = false;
2641 self.rebuild_availability_snapshot();
2642 }
2643 }
2644 _ = soft_reap_interval.tick() => {
2646 let soft_timeout = self.config.connect_soft_timeout;
2647 if soft_timeout > 0 {
2648 let candidates = self.peer_states.as_ref().map_or_else(
2650 Vec::new,
2651 |ps| ps.soft_reap_candidates(Duration::from_secs(soft_timeout)),
2652 );
2653 for peer_addr in candidates {
2654 debug!(%peer_addr, soft_timeout, "soft reap: no TCP SYN-ACK");
2655 self.connect_permits.remove(&peer_addr);
2657 self.disconnect_peer(peer_addr, "soft reap: no TCP SYN-ACK");
2658 if let Some(ref ps) = self.peer_states
2659 && let Some(backoff) = ps.mark_dead(peer_addr)
2660 {
2661 let ps_clone = Arc::clone(ps);
2662 tokio::spawn(async move {
2663 tokio::time::sleep(backoff).await;
2664 ps_clone.mark_queued_for_retry(peer_addr);
2665 });
2666 }
2667 }
2668 }
2669 }
2670 _ = eviction_interval.tick() => {
2672 if self.state != TorrentState::Seeding {
2673 let should_evict = if let Some(ref ps) = self.peer_states {
2675 let live = ps.stats.live.load(std::sync::atomic::Ordering::Relaxed);
2676 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2677 let threshold = (self.effective_max_connections() as f32 * 0.75) as u32;
2678 live >= threshold
2679 } else {
2680 false
2681 };
2682 if should_evict {
2683 for _ in 0..5 {
2685 match self.find_eviction_candidate() {
2686 Some((victim, pass)) => {
2687 debug!(%victim, ?pass, "M148 proactive eviction");
2688 self.disconnect_peer(victim, "proactive eviction");
2689 if matches!(pass, crate::torrent_peers::EvictionPass::ZeroThroughput)
2690 && let Some(ref ps) = self.peer_states
2691 {
2692 ps.add_eviction_ban(victim);
2693 }
2694 }
2695 None => break,
2696 }
2697 }
2698 }
2699
2700 self.run_piece_steal_scan();
2702 }
2703 }
2704 _ = refill_interval.tick() => {
2706 let elapsed = Duration::from_millis(100);
2707 self.upload_bucket.refill(elapsed);
2708 self.download_bucket.refill(elapsed);
2709 self.rate_limiter_set.refill(elapsed);
2711 let (tcp_peers, utp_peers) = self.transport_peer_counts();
2712 self.rate_limiter_set.apply_mixed_mode(
2713 self.config.mixed_mode_algorithm,
2714 tcp_peers,
2715 utp_peers,
2716 self.config.upload_rate_limit,
2717 );
2718 }
2719 }
2720
2721 for target in std::mem::take(&mut self.holepunch_pending) {
2723 self.try_holepunch(target).await;
2724 }
2725 }
2726 }
2727
2728 pub(crate) fn distributed_copies(&self) -> (u32, u32, f32) {
2734 if self.num_pieces == 0 || self.peers.is_empty() {
2735 return (0, 0, 0.0);
2736 }
2737
2738 let num = self.num_pieces as usize;
2739 let mut availability = vec![0u32; num];
2740
2741 for peer in self.peers.values() {
2742 for idx in 0..self.num_pieces {
2743 if peer.bitfield.get(idx) {
2744 availability[idx as usize] += 1;
2745 }
2746 }
2747 }
2748
2749 let min_avail = availability.iter().copied().min().unwrap_or(0);
2750 let rarest_count = availability.iter().filter(|&&c| c == min_avail).count() as u32;
2751 let fraction = ((self.num_pieces - rarest_count) * 1000) / self.num_pieces;
2752 let copies_float = min_avail as f32 + fraction as f32 / 1000.0;
2753
2754 (min_avail, fraction, copies_float)
2755 }
2756
2757 fn build_download_queue(&self) -> Vec<PartialPieceInfo> {
2758 self.piece_owner
2759 .iter()
2760 .enumerate()
2761 .filter_map(|(piece_index, owner)| {
2762 owner.map(|_| {
2763 let piece_index = piece_index as u32;
2764 let blocks_in_piece = self
2765 .lengths
2766 .as_ref()
2767 .map(|l| l.piece_size(piece_index).div_ceil(l.chunk_size()))
2768 .unwrap_or(0);
2769 PartialPieceInfo {
2770 piece_index,
2771 blocks_in_piece,
2772 blocks_assigned: 0,
2773 }
2774 })
2775 })
2776 .collect()
2777 }
2778
2779 fn compute_file_progress(&self) -> Vec<u64> {
2781 let meta = match self.meta.as_ref() {
2782 Some(m) => m,
2783 None => return Vec::new(),
2784 };
2785 let lengths = match self.lengths.as_ref() {
2786 Some(l) => l,
2787 None => return Vec::new(),
2788 };
2789 let chunk_tracker = match self.chunk_tracker.as_ref() {
2790 Some(ct) => ct,
2791 None => return Vec::new(),
2792 };
2793
2794 let files = meta.info.files();
2795 if files.is_empty() {
2796 return Vec::new();
2797 }
2798
2799 let piece_length = lengths.piece_length();
2800 let mut result = Vec::with_capacity(files.len());
2801 let mut file_offset = 0u64;
2802
2803 for file_entry in &files {
2804 let file_len = file_entry.length;
2805 if file_len == 0 {
2806 result.push(0);
2807 file_offset += file_len;
2808 continue;
2809 }
2810
2811 let file_end = file_offset + file_len;
2812 let first_piece = (file_offset / piece_length) as u32;
2813 let last_piece = ((file_end - 1) / piece_length) as u32;
2814
2815 let mut downloaded = 0u64;
2816
2817 for p in first_piece..=last_piece {
2818 if !chunk_tracker.has_piece(p) {
2819 continue;
2820 }
2821
2822 let piece_start = lengths.piece_offset(p);
2823 let piece_end = piece_start + lengths.piece_size(p) as u64;
2824
2825 let overlap_start = piece_start.max(file_offset);
2827 let overlap_end = piece_end.min(file_end);
2828
2829 if overlap_start < overlap_end {
2830 downloaded += overlap_end - overlap_start;
2831 }
2832 }
2833
2834 result.push(downloaded);
2835 file_offset = file_end;
2836 }
2837
2838 result
2839 }
2840
2841 fn v6_retry_delay(&self) -> std::time::Duration {
2844 let base_ms: u64 = 100;
2845 let max_ms: u64 = 5000;
2846 let delay_ms = base_ms
2847 .saturating_mul(
2848 1u64.checked_shl(self.dht_v6_empty_count)
2849 .unwrap_or(u64::MAX),
2850 )
2851 .min(max_ms);
2852 std::time::Duration::from_millis(delay_ms)
2853 }
2854
2855 fn should_retry_v6(&self) -> bool {
2857 let Some(last) = self.dht_v6_last_retry else {
2858 return true; };
2860 last.elapsed() >= self.v6_retry_delay()
2861 }
2862
2863 async fn handle_force_dht_announce(&self) {
2865 if let Some(ref dht) = self.dht
2866 && let Err(e) = dht.announce(self.info_hash, self.config.listen_port).await
2867 {
2868 warn!("Force DHT v4 announce failed: {e}");
2869 }
2870 if let Some(ref dht6) = self.dht_v6
2871 && let Err(e) = dht6.announce(self.info_hash, self.config.listen_port).await
2872 {
2873 debug!("Force DHT v6 announce failed: {e}");
2874 }
2875 if self.info_hashes.is_hybrid()
2877 && let Some(v2) = self.info_hashes.v2
2878 {
2879 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
2880 if v2_as_v1 != self.info_hash {
2881 if let Some(ref dht) = self.dht
2882 && let Err(e) = dht.announce(v2_as_v1, self.config.listen_port).await
2883 {
2884 debug!("Force DHT v4 dual-swarm announce failed: {e}");
2885 }
2886 if let Some(ref dht6) = self.dht_v6
2887 && let Err(e) = dht6.announce(v2_as_v1, self.config.listen_port).await
2888 {
2889 debug!("Force DHT v6 dual-swarm announce failed: {e}");
2890 }
2891 }
2892 }
2893 }
2894
2895 async fn run_dht_requery(&mut self) {
2901 if !self.config.enable_dht {
2902 return;
2903 }
2904
2905 if self.peers.len() > self.config.max_peers.saturating_mul(4) {
2909 return;
2910 }
2911
2912 if self.dht_peers_rx.is_none()
2920 && let Some(ref dht) = self.dht
2921 {
2922 match dht.get_peers(self.info_hash).await {
2923 Ok(rx) => self.dht_peers_rx = Some(rx),
2924 Err(e) => warn!("DHT v4 re-query failed: {e}"),
2925 }
2926 }
2927
2928 if self.dht_v6_peers_rx.is_none()
2930 && self.dht_v6_empty_count < 30
2931 && self.should_retry_v6()
2932 && let Some(ref dht6) = self.dht_v6
2933 {
2934 self.dht_v6_last_retry = Some(std::time::Instant::now());
2935 match dht6.get_peers(self.info_hash).await {
2936 Ok(rx) => self.dht_v6_peers_rx = Some(rx),
2937 Err(e) => debug!("DHT v6 re-query failed: {e}"),
2938 }
2939 }
2940
2941 if self.info_hashes.is_hybrid()
2943 && let Some(v2) = self.info_hashes.v2
2944 {
2945 let v2_bytes: [u8; 20] = v2.0[..20]
2946 .try_into()
2947 .expect("Id32 is 32 bytes; first 20 always fit");
2948 let v2_as_v1 = Id20(v2_bytes);
2949
2950 if self.dht_v2_peers_rx.is_none()
2951 && let Some(ref dht) = self.dht
2952 {
2953 match dht.get_peers(v2_as_v1).await {
2954 Ok(rx) => self.dht_v2_peers_rx = Some(rx),
2955 Err(e) => debug!("DHT v4 v2-swarm re-query failed: {e}"),
2956 }
2957 }
2958 if self.dht_v6_v2_peers_rx.is_none()
2959 && self.dht_v6_empty_count < 30
2960 && self.should_retry_v6()
2961 && let Some(ref dht6) = self.dht_v6
2962 {
2963 self.dht_v6_last_retry = Some(std::time::Instant::now());
2964 match dht6.get_peers(v2_as_v1).await {
2965 Ok(rx) => self.dht_v6_v2_peers_rx = Some(rx),
2966 Err(e) => debug!("DHT v6 v2-swarm re-query failed: {e}"),
2967 }
2968 }
2969 }
2970
2971 debug!(peers = self.peers.len(), "DHT re-query triggered");
2972 }
2973
2974 async fn handle_read_piece(&self, index: u32) -> crate::Result<Bytes> {
2976 let disk = self
2977 .disk
2978 .as_ref()
2979 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
2980 let lengths = self
2981 .lengths
2982 .as_ref()
2983 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
2984
2985 let piece_size = lengths.piece_size(index);
2986 if piece_size == 0 {
2987 return Err(crate::Error::InvalidPieceIndex {
2988 index,
2989 num_pieces: lengths.num_pieces(),
2990 });
2991 }
2992
2993 let chunk_size = lengths.chunk_size();
2994 let num_chunks = lengths.chunks_in_piece(index);
2995 let mut buf = bytes::BytesMut::with_capacity(piece_size as usize);
2996
2997 for chunk_idx in 0..num_chunks {
2998 let begin = chunk_idx * chunk_size;
2999 let len = if chunk_idx == num_chunks - 1 {
3000 piece_size - begin
3001 } else {
3002 chunk_size
3003 };
3004 let data = disk
3005 .read_chunk(index, begin, len, DiskJobFlags::empty())
3006 .await
3007 .map_err(crate::Error::Storage)?;
3008 buf.extend_from_slice(&data);
3009 }
3010
3011 Ok(buf.freeze())
3012 }
3013
3014 async fn handle_flush_cache(&self) -> crate::Result<()> {
3016 let disk = self
3017 .disk
3018 .as_ref()
3019 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3020 disk.flush_cache().await.map_err(crate::Error::Storage)
3021 }
3022
3023 fn handle_connect_peer(&mut self, addr: SocketAddr) {
3025 if self.peers.contains_key(&addr) {
3027 return;
3028 }
3029 if let Some(ref ps) = self.peer_states {
3031 ps.add_if_not_seen(addr, PeerSource::Incoming);
3032 }
3033 }
3034
3035 pub(crate) fn fire_tracker_alerts(&self, outcomes: &[crate::tracker_manager::TrackerOutcome]) {
3037 for outcome in outcomes {
3038 match &outcome.result {
3039 Ok(num_peers) => {
3040 post_alert(
3041 &self.alert_tx,
3042 &self.alert_mask,
3043 AlertKind::TrackerReply {
3044 info_hash: self.info_hash,
3045 url: outcome.url.clone(),
3046 num_peers: *num_peers,
3047 },
3048 );
3049 }
3050 Err(msg) => {
3051 post_alert(
3052 &self.alert_tx,
3053 &self.alert_mask,
3054 AlertKind::TrackerError {
3055 info_hash: self.info_hash,
3056 url: outcome.url.clone(),
3057 message: msg.clone(),
3058 },
3059 );
3060 }
3061 }
3062 }
3063 }
3064
3065 pub(crate) fn calculate_left(&self) -> u64 {
3067 match (&self.meta, &self.chunk_tracker) {
3068 (Some(meta), Some(ct)) => {
3069 let total = meta.info.total_length();
3070 let have = ct.bitfield().count_ones() as u64;
3071 let pieces_total = self.num_pieces as u64;
3072 if pieces_total == 0 {
3073 total
3074 } else {
3075 total.saturating_sub(have * (total / pieces_total))
3076 }
3077 }
3078 _ => 0,
3079 }
3080 }
3081
3082 pub(crate) async fn shutdown_peers(&mut self) {
3083 let left = self.calculate_left();
3085 let _ = tokio::time::timeout(
3086 std::time::Duration::from_secs(3),
3087 self.tracker_manager
3088 .announce_stopped(self.uploaded, self.downloaded, left),
3089 )
3090 .await;
3091
3092 for peer in self.peers.values() {
3094 let _ = peer.cmd_tx.try_send(PeerCommand::Shutdown);
3095 }
3096 }
3097
3098 pub(crate) async fn handle_piece_data(
3101 &mut self,
3102 peer_addr: SocketAddr,
3103 index: u32,
3104 begin: u32,
3105 data: Bytes,
3106 ) {
3107 if let Some(ref ct) = self.chunk_tracker
3111 && ct.has_chunk(index, begin)
3112 {
3113 self.total_download += data.len() as u64 + 13;
3114 if let Some(peer) = self.peers.get_mut(&peer_addr) {
3118 peer.pending_requests.remove(index, begin);
3119 }
3120 if self.end_game.is_active() {
3124 self.end_game.block_received(index, begin, peer_addr);
3125 }
3126 return;
3128 }
3129
3130 let data_len = data.len();
3131
3132 if let Some(ref disk) = self.disk {
3134 disk.write_block_deferred(index, begin, data);
3135 }
3136
3137 self.downloaded += data_len as u64;
3138 self.total_download += data_len as u64 + 13; self.last_download = now_unix();
3140 self.need_save_resume = true;
3141
3142 if let Some(slab_idx) = self.peer_slab.slot_of(&peer_addr)
3144 && self.piece_owner.get(index as usize) == Some(&None)
3145 {
3146 self.piece_owner[index as usize] = Some(slab_idx);
3147 if self.inflight_started.get(index as usize) == Some(&None) {
3149 self.inflight_started[index as usize] = Some(Instant::now());
3150 }
3151 if let (Some(sc), Some(bm)) = (&self.steal_candidates, &self.block_maps)
3153 && let Some(lengths) = &self.lengths
3154 {
3155 let total_blocks = lengths.chunks_in_piece(index);
3156 if bm.next_unrequested(index, total_blocks).is_some() {
3157 sc.push(index);
3158 }
3159 }
3160 }
3161
3162 self.piece_contributors
3164 .entry(index)
3165 .or_default()
3166 .insert(peer_addr.ip());
3167
3168 let now = std::time::Instant::now();
3169 if let Some(peer) = self.peers.get_mut(&peer_addr) {
3170 peer.pending_requests.remove(index, begin);
3171 peer.download_bytes_window += data_len as u64;
3172 peer.download_bytes_total += data_len as u64;
3173 peer.pipeline
3174 .block_received(index, begin, data_len as u32, now);
3175 peer.last_data_received = Some(now);
3176 if peer.snubbed {
3178 peer.snubbed = false;
3179 }
3180 }
3181 if self.end_game.is_active() {
3186 let cancels = self.end_game.block_received(index, begin, peer_addr);
3187 for (cancel_addr, ci, cb, cl) in cancels {
3188 if let Some(cancel_peer) = self.peers.get_mut(&cancel_addr) {
3189 let _ = cancel_peer.cmd_tx.try_send(PeerCommand::Cancel {
3190 index: ci,
3191 begin: cb,
3192 length: cl,
3193 });
3194 cancel_peer.pending_requests.remove(ci, cb);
3195 }
3196 }
3197 }
3198
3199 let piece_complete = if let Some(ref mut ct) = self.chunk_tracker {
3201 ct.chunk_received(index, begin)
3202 } else {
3203 false
3204 };
3205
3206 if piece_complete && !self.pending_verify.contains(&index) {
3207 if self.config.predictive_piece_announce_ms > 0
3209 && !self.predictive_have_sent.contains(&index)
3210 {
3211 self.predictive_have_sent.insert(index);
3212 let _ = self.have_broadcast_tx.send(index);
3213 }
3214
3215 if let Some(ref disk) = self.disk {
3218 disk.flush_piece_writes(index).await;
3219 }
3220
3221 match self.version {
3222 irontide_core::TorrentVersion::V1Only => {
3223 if let Some(ref disk) = self.disk
3225 && let Some(expected) = self
3226 .meta
3227 .as_ref()
3228 .and_then(|m| m.info.piece_hash(index as usize))
3229 {
3230 self.pending_verify.insert(index);
3231 let generation = self
3232 .piece_generations
3233 .get(index as usize)
3234 .copied()
3235 .unwrap_or(0);
3236 disk.enqueue_verify(index, expected, generation, &self.verify_result_tx);
3237 }
3238 }
3239 irontide_core::TorrentVersion::V2Only => {
3240 self.verify_and_mark_piece_v2(index).await;
3242 }
3243 irontide_core::TorrentVersion::Hybrid => {
3244 self.verify_and_mark_piece_hybrid(index).await;
3246 }
3247 }
3248 }
3249
3250 if self.end_game.is_active() {
3253 self.request_end_game_block(peer_addr).await;
3254 }
3255 }
3256
3257 pub(crate) async fn handle_piece_blocks_batch(
3262 &mut self,
3263 peer_addr: SocketAddr,
3264 blocks: Vec<crate::types::BlockEntry>,
3265 ) {
3266 for block in &blocks {
3267 self.process_block_completion(peer_addr, block.index, block.begin, block.length)
3268 .await;
3269 }
3270 }
3271
3272 fn handle_open_file(
3273 &mut self,
3274 file_index: usize,
3275 ) -> crate::Result<crate::streaming::FileStreamHandle> {
3276 let meta = self
3277 .meta
3278 .as_ref()
3279 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3280 let files = meta.info.files();
3281 if file_index >= files.len() {
3282 return Err(crate::Error::InvalidFileIndex {
3283 index: file_index,
3284 count: files.len(),
3285 });
3286 }
3287 if self.file_priorities.get(file_index).copied() == Some(FilePriority::Skip) {
3288 return Err(crate::Error::FileSkipped { index: file_index });
3289 }
3290
3291 let lengths = self
3292 .lengths
3293 .as_ref()
3294 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3295 let disk = self
3296 .disk
3297 .as_ref()
3298 .ok_or(crate::Error::MetadataNotReady(self.info_hash))?;
3299
3300 let mut file_offset = 0u64;
3302 for f in &files[..file_index] {
3303 file_offset += f.length;
3304 }
3305 let file_length = files[file_index].length;
3306
3307 let (cursor_tx, cursor_rx) = tokio::sync::watch::channel(0u64);
3308
3309 let permit = self
3310 .stream_read_semaphore
3311 .clone()
3312 .try_acquire_owned()
3313 .map_err(|_| crate::Error::Connection("too many concurrent stream readers".into()))?;
3314
3315 self.streaming_cursors
3317 .push(crate::streaming::StreamingCursor {
3318 file_index,
3319 file_offset,
3320 cursor_piece: (file_offset / lengths.piece_length()) as u32,
3321 readahead_pieces: self.config.readahead_pieces,
3322 cursor_rx,
3323 });
3324
3325 Ok(crate::streaming::FileStreamHandle {
3326 disk: disk.clone(),
3327 lengths: lengths.clone(),
3328 file_index,
3329 file_offset,
3330 file_length,
3331 cursor_tx,
3332 piece_ready_rx: self.piece_ready_tx.subscribe(),
3333 have: self.have_watch_rx.clone(),
3334 read_permit: permit,
3335 })
3336 }
3337
3338 async fn suggest_cached_pieces(&mut self) {
3340 if !self.config.suggest_mode {
3341 return;
3342 }
3343 let disk = match self.disk {
3344 Some(ref d) => d.clone(),
3345 None => return,
3346 };
3347 let cached = disk.cached_pieces().await;
3348 if cached.is_empty() {
3349 return;
3350 }
3351 let max_suggest = self.config.max_suggest_pieces;
3352 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
3353 for peer_addr in peer_addrs {
3354 let already_suggested = self.suggested_to_peers.entry(peer_addr).or_default();
3355 let peer_has_piece = |piece: u32| -> bool {
3356 self.peers
3357 .get(&peer_addr)
3358 .is_some_and(|p| p.bitfield.get(piece))
3359 };
3360 let mut sent = 0;
3361 for &piece in &cached {
3362 if sent >= max_suggest {
3363 break;
3364 }
3365 if peer_has_piece(piece) {
3366 continue;
3367 }
3368 if already_suggested.contains(&piece) {
3369 continue;
3370 }
3371 if let Some(peer) = self.peers.get(&peer_addr) {
3372 let _ = peer.cmd_tx.try_send(PeerCommand::SuggestPiece(piece));
3373 already_suggested.insert(piece);
3374 sent += 1;
3375 }
3376 }
3377 }
3378 }
3379
3380 async fn handle_pre_resolved_metadata(&mut self, info_bytes: Vec<u8>, peers: Vec<SocketAddr>) {
3386 if self.state != TorrentState::FetchingMetadata {
3388 debug!(
3389 info_hash = %self.info_hash,
3390 state = ?self.state,
3391 "ignoring pre-resolved metadata: already past FetchingMetadata"
3392 );
3393 return;
3394 }
3395
3396 debug!(
3397 info_hash = %self.info_hash,
3398 info_bytes_len = info_bytes.len(),
3399 num_peers = peers.len(),
3400 "received pre-resolved metadata from background resolver"
3401 );
3402
3403 if let Some(ref mut dl) = self.metadata_downloader {
3405 dl.set_total_size(info_bytes.len() as u64);
3407
3408 let piece_size: usize = 16384;
3412 let num_pieces = info_bytes.len().div_ceil(piece_size);
3413 for i in 0..num_pieces {
3414 let start = i * piece_size;
3415 let end = (start + piece_size).min(info_bytes.len());
3416 let data = bytes::Bytes::copy_from_slice(&info_bytes[start..end]);
3417 dl.piece_received(i as u32, data);
3418 }
3419 }
3420
3421 self.try_assemble_metadata().await;
3424
3425 if !peers.is_empty() {
3427 self.handle_add_peers(peers, crate::peer_state::PeerSource::Dht);
3428 }
3429 }
3430
3431 pub(crate) async fn try_assemble_metadata(&mut self) {
3432 let assembled = if let Some(ref dl) = self.metadata_downloader {
3433 dl.assemble_and_verify()
3434 } else {
3435 return;
3436 };
3437
3438 match assembled {
3439 Ok(info_bytes) => {
3440 let mut torrent_bytes = b"d4:info".to_vec();
3446 torrent_bytes.extend_from_slice(&info_bytes);
3447 torrent_bytes.push(b'e');
3448
3449 match torrent_from_bytes(&torrent_bytes) {
3450 Ok(meta) => {
3451 let num_pieces = meta.info.num_pieces() as u32;
3452 let lengths = Lengths::new(
3453 meta.info.total_length(),
3454 meta.info.piece_length,
3455 DEFAULT_CHUNK_SIZE,
3456 );
3457
3458 let files = meta.info.files();
3460 let file_paths: Vec<std::path::PathBuf> = files
3461 .iter()
3462 .map(|f| f.path.iter().collect::<std::path::PathBuf>())
3463 .collect();
3464 let file_lengths_vec: Vec<u64> = files.iter().map(|f| f.length).collect();
3465 let prealloc_mode = self.config.preallocate_mode.unwrap_or_else(|| {
3466 irontide_storage::PreallocateMode::from(
3467 self.config.storage_mode == irontide_core::StorageMode::Full,
3468 )
3469 });
3470 let storage: Arc<dyn TorrentStorage> =
3471 match irontide_storage::FilesystemStorage::new(
3472 &self.config.download_dir,
3473 file_paths,
3474 file_lengths_vec,
3475 lengths.clone(),
3476 None,
3477 prealloc_mode,
3478 self.config.filesystem_direct_io,
3479 ) {
3480 Ok(s) => Arc::new(s),
3481 Err(e) => {
3482 warn!(
3483 "failed to create filesystem storage: {e}, falling back to memory"
3484 );
3485 Arc::new(MemoryStorage::new(lengths.clone()))
3486 }
3487 };
3488 let mut disk_handle = self
3489 .disk_manager
3490 .register_torrent(self.info_hash, storage)
3491 .await;
3492
3493 self.chunk_tracker = Some(ChunkTracker::new(lengths.clone()));
3494 self.lengths = Some(lengths);
3495 self.num_pieces = num_pieces;
3496 self.piece_generations = vec![0u64; num_pieces as usize];
3498 let (hash_tx, hash_rx) = tokio::sync::mpsc::channel(64);
3499 self.hash_result_tx = hash_tx;
3500 self.hash_result_rx = hash_rx;
3501 if let Some(ref pool) = self.hash_pool_ref {
3504 disk_handle.set_hash_pool(pool.clone());
3505 disk_handle.set_hash_result_tx(self.hash_result_tx.clone());
3506 }
3507 self.disk = Some(disk_handle);
3508 for peer in self.peers.values() {
3511 let _ = peer
3512 .cmd_tx
3513 .try_send(PeerCommand::UpdateNumPieces(num_pieces));
3514 }
3515 let file_lengths: Vec<u64> =
3516 meta.info.files().iter().map(|f| f.length).collect();
3517 let mut meta = meta;
3518 meta.info_bytes = Some(Bytes::from(info_bytes));
3519 self.meta = Some(meta);
3520
3521 if let (Some(meta), Some(lengths)) = (&self.meta, &self.lengths) {
3523 self.cached_files = Some(build_cached_file_info(meta, lengths));
3524 }
3525
3526 self.file_priorities = vec![FilePriority::Normal; file_lengths.len()];
3527
3528 if let Some(ref selections) = self.magnet_selected_files {
3530 self.file_priorities = irontide_core::FileSelection::to_priorities(
3531 selections,
3532 file_lengths.len(),
3533 );
3534 self.magnet_selected_files = None;
3535 }
3536
3537 self.wanted_pieces = crate::piece_selector::build_wanted_pieces(
3538 &self.file_priorities,
3539 &file_lengths,
3540 self.lengths.as_ref().unwrap(),
3541 );
3542 if self.config.share_mode {
3543 self.transition_state(TorrentState::Sharing);
3544 } else {
3545 self.transition_state(TorrentState::Downloading);
3546 }
3547 self.metadata_downloader = None;
3548
3549 if let Some(ref meta) = self.meta {
3551 self.tracker_manager
3552 .set_metadata_filtered(meta, &self.config.url_security);
3553 }
3554
3555 if let Ok(detected) = irontide_core::torrent_from_bytes_any(&torrent_bytes)
3558 {
3559 let new_version = detected.version();
3560 if new_version != irontide_core::TorrentVersion::V1Only {
3561 let new_hashes = detected.info_hashes();
3562 self.version = new_version;
3563 self.info_hashes = new_hashes.clone();
3564 self.tracker_manager.set_info_hashes(new_hashes.clone());
3565 if let Some(v2_meta) = detected.as_v2() {
3566 self.meta_v2 = Some(v2_meta.clone());
3567 }
3568 if new_hashes.is_hybrid()
3570 && let Some(v2) = new_hashes.v2
3571 {
3572 let v2_as_v1 = Id20(v2.0[..20].try_into().unwrap());
3573 if v2_as_v1 != self.info_hash {
3574 if self.dht_v2_peers_rx.is_none()
3575 && let Some(ref dht) = self.dht
3576 && let Ok(rx) = dht.get_peers(v2_as_v1).await
3577 {
3578 self.dht_v2_peers_rx = Some(rx);
3579 }
3580 if self.dht_v6_v2_peers_rx.is_none()
3581 && self.dht_v6_empty_count < 30
3582 && self.should_retry_v6()
3583 && let Some(ref dht6) = self.dht_v6
3584 && let Ok(rx) = dht6.get_peers(v2_as_v1).await
3585 {
3586 self.dht_v6_last_retry =
3587 Some(std::time::Instant::now());
3588 self.dht_v6_v2_peers_rx = Some(rx);
3589 }
3590 }
3591 }
3592 }
3593 }
3594
3595 let name = self
3596 .meta
3597 .as_ref()
3598 .map(|m| m.info.name.clone())
3599 .unwrap_or_default();
3600 post_alert(
3601 &self.alert_tx,
3602 &self.alert_mask,
3603 AlertKind::MetadataReceived {
3604 info_hash: self.info_hash,
3605 name,
3606 },
3607 );
3608 info!("metadata assembled, switching to Downloading");
3609
3610 if let Some(ct) = &self.chunk_tracker {
3612 let atomic_states = Arc::new(AtomicPieceStates::new(
3613 self.num_pieces,
3614 ct.bitfield(),
3615 &self.wanted_pieces,
3616 ));
3617 self.atomic_states = Some(Arc::clone(&atomic_states));
3618 self.availability = vec![0u32; self.num_pieces as usize];
3619 self.piece_owner = vec![None; self.num_pieces as usize];
3620 self.inflight_started = vec![None; self.num_pieces as usize];
3622 self.max_in_flight = self.config.max_in_flight_pieces;
3623
3624 if self.config.use_block_stealing {
3626 if let Some(ref lengths) = self.lengths {
3627 self.block_maps =
3628 Some(Arc::new(BlockMaps::new(self.num_pieces, lengths)));
3629 }
3630 self.steal_candidates = Some(Arc::new(StealCandidates::new()));
3631 }
3632 self.piece_write_guards = Some(Arc::new(
3634 crate::piece_reservation::PieceWriteGuards::new(self.num_pieces),
3635 ));
3636
3637 let snapshot = Arc::new(AvailabilitySnapshot::build(
3638 &self.availability,
3639 &atomic_states,
3640 &self.priority_pieces,
3641 0,
3642 ));
3643 self.availability_snapshot = Some(snapshot);
3644 self.snapshot_generation = 0;
3645
3646 let notify = Arc::new(tokio::sync::Notify::new());
3647 self.reservation_notify = Some(notify);
3648 }
3649
3650 self.spawn_web_seeds();
3652 self.assign_pieces_to_web_seeds();
3653
3654 let peer_addrs: Vec<SocketAddr> = self.peers.keys().copied().collect();
3657 info!(
3658 connected_peers = peer_addrs.len(),
3659 "kick-starting piece requests for pre-connected peers"
3660 );
3661 for addr in peer_addrs {
3662 let has_bitfield = self
3663 .peers
3664 .get(&addr)
3665 .map(|p| p.bitfield.count_ones())
3666 .unwrap_or(0);
3667 let is_choking = self
3668 .peers
3669 .get(&addr)
3670 .map(|p| p.peer_choking)
3671 .unwrap_or(true);
3672 debug!(%addr, has_bitfield, is_choking, "post-metadata peer state");
3673 self.maybe_express_interest(addr).await;
3674 if let Some(peer) = self.peers.get(&addr)
3676 && peer.bitfield.count_ones() > 0
3677 {
3678 for piece in 0..self.num_pieces {
3679 if peer.bitfield.get(piece) {
3680 self.availability[piece as usize] += 1;
3681 }
3682 }
3683 let _slot = self.peer_slab.insert(addr);
3684 }
3685 }
3686 self.recalc_max_in_flight();
3687 self.rebuild_availability_snapshot();
3688 if !self.user_seed_mode
3692 && let (Some(atomic_states), Some(snapshot), Some(notify)) = (
3693 &self.atomic_states,
3694 &self.availability_snapshot,
3695 &self.reservation_notify,
3696 )
3697 && let Some(ref lengths) = self.lengths
3698 {
3699 for peer in self.peers.values() {
3700 let _ = peer.cmd_tx.try_send(PeerCommand::StartRequesting {
3701 atomic_states: Arc::clone(atomic_states),
3702 availability_snapshot: Arc::clone(snapshot),
3703 piece_notify: Arc::clone(notify),
3704 disk_handle: self.disk.clone(),
3705 write_error_tx: self.write_error_tx.clone(),
3706 lengths: lengths.clone(),
3707 block_maps: self.block_maps.clone(),
3708 steal_candidates: self.steal_candidates.clone(),
3709 piece_write_guards: self.piece_write_guards.clone(),
3710 });
3711 }
3712 }
3713 }
3714 Err(e) => {
3715 warn!("failed to parse assembled metadata: {e}");
3716 post_alert(
3717 &self.alert_tx,
3718 &self.alert_mask,
3719 AlertKind::MetadataFailed {
3720 info_hash: self.info_hash,
3721 },
3722 );
3723 }
3724 }
3725 }
3726 Err(e) => {
3727 warn!("metadata assembly failed: {e}");
3728 post_alert(
3729 &self.alert_tx,
3730 &self.alert_mask,
3731 AlertKind::MetadataFailed {
3732 info_hash: self.info_hash,
3733 },
3734 );
3735 }
3736 }
3737 }
3738
3739 fn spawn_web_seeds(&mut self) {
3742 if !self.config.enable_web_seed {
3743 return;
3744 }
3745 let meta = match &self.meta {
3746 Some(m) => m,
3747 None => return,
3748 };
3749 let lengths = match &self.lengths {
3750 Some(l) => l.clone(),
3751 None => return,
3752 };
3753
3754 let file_lengths: Vec<u64> = meta.info.files().iter().map(|f| f.length).collect();
3755 let file_map = irontide_storage::FileMap::new(file_lengths, lengths.clone());
3756
3757 for url in &meta.url_list {
3759 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
3760 continue;
3761 }
3762 if self.web_seeds.len() >= self.config.max_web_seeds {
3763 break;
3764 }
3765
3766 if let Err(e) = crate::url_guard::validate_web_seed_url(url, &self.config.url_security)
3768 {
3769 warn!(%url, %e, "web seed URL rejected by security policy");
3770 continue;
3771 }
3772
3773 let url_builder = if meta.info.length.is_some() {
3774 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone())
3775 } else {
3776 let file_paths: Vec<String> = meta
3777 .info
3778 .files()
3779 .iter()
3780 .map(|f| f.path[1..].join("/")) .collect();
3782 crate::web_seed::WebSeedUrlBuilder::multi(
3783 url.clone(),
3784 meta.info.name.clone(),
3785 file_paths,
3786 )
3787 };
3788
3789 let (cmd_tx, cmd_rx) = mpsc::channel(16);
3790 let task = crate::web_seed::WebSeedTask::new(
3791 url.clone(),
3792 crate::web_seed::WebSeedMode::GetRight,
3793 url_builder,
3794 lengths.clone(),
3795 file_map.clone(),
3796 self.info_hash,
3797 cmd_rx,
3798 self.event_tx.clone(),
3799 &self.config.url_security,
3800 );
3801 tokio::spawn(task.run());
3802 self.web_seeds.insert(url.clone(), cmd_tx);
3803 debug!(url, "spawned BEP 19 web seed");
3804 }
3805
3806 for url in &meta.httpseeds {
3808 if self.banned_web_seeds.contains(url) || self.web_seeds.contains_key(url) {
3809 continue;
3810 }
3811 if self.web_seeds.len() >= self.config.max_web_seeds {
3812 break;
3813 }
3814
3815 if let Err(e) = crate::url_guard::validate_web_seed_url(url, &self.config.url_security)
3817 {
3818 warn!(%url, %e, "web seed URL rejected by security policy");
3819 continue;
3820 }
3821
3822 let url_builder =
3824 crate::web_seed::WebSeedUrlBuilder::single(url.clone(), meta.info.name.clone());
3825
3826 let (cmd_tx, cmd_rx) = mpsc::channel(16);
3827 let task = crate::web_seed::WebSeedTask::new(
3828 url.clone(),
3829 crate::web_seed::WebSeedMode::Hoffman,
3830 url_builder,
3831 lengths.clone(),
3832 file_map.clone(),
3833 self.info_hash,
3834 cmd_rx,
3835 self.event_tx.clone(),
3836 &self.config.url_security,
3837 );
3838 tokio::spawn(task.run());
3839 self.web_seeds.insert(url.clone(), cmd_tx);
3840 debug!(url, "spawned BEP 17 web seed");
3841 }
3842 }
3843
3844 fn assign_pieces_to_web_seeds(&mut self) {
3845 if self.state != TorrentState::Downloading || self.end_game.is_active() {
3846 return;
3847 }
3848
3849 let active_urls: HashSet<&String> = self.web_seed_in_flight.values().collect();
3851 let idle_urls: Vec<String> = self
3852 .web_seeds
3853 .keys()
3854 .filter(|u| !active_urls.contains(u))
3855 .cloned()
3856 .collect();
3857
3858 let ct = match &self.chunk_tracker {
3859 Some(ct) => ct,
3860 None => return,
3861 };
3862
3863 for url in idle_urls {
3864 let piece = (0..self.num_pieces).find(|&i| {
3867 !ct.has_piece(i)
3868 && !self
3869 .piece_owner
3870 .get(i as usize)
3871 .is_some_and(|o| o.is_some())
3872 && !self.web_seed_in_flight.contains_key(&i)
3873 && self.wanted_pieces.get(i)
3874 });
3875
3876 if let Some(piece) = piece
3877 && let Some(cmd_tx) = self.web_seeds.get(&url)
3878 {
3879 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::FetchPiece(piece));
3880 self.web_seed_in_flight.insert(piece, url);
3881 }
3882 }
3883 }
3884
3885 pub(crate) async fn handle_web_seed_piece_data(
3886 &mut self,
3887 url: String,
3888 index: u32,
3889 data: Bytes,
3890 ) {
3891 self.web_seed_in_flight.remove(&index);
3892
3893 if let Some(ref ct) = self.chunk_tracker
3895 && ct.has_piece(index)
3896 {
3897 self.assign_pieces_to_web_seeds();
3898 return;
3899 }
3900
3901 if let Some(ref disk) = self.disk
3903 && let Err(e) = disk
3904 .write_chunk(index, 0, data.clone(), DiskJobFlags::FLUSH_PIECE)
3905 .await
3906 {
3907 warn!(index, "web seed: failed to write piece: {e}");
3908 self.assign_pieces_to_web_seeds();
3909 return;
3910 }
3911
3912 if let Some(ref mut ct) = self.chunk_tracker
3914 && let Some(ref lengths) = self.lengths
3915 {
3916 let num_chunks = lengths.chunks_in_piece(index);
3917 for chunk_idx in 0..num_chunks {
3918 if let Some((begin, _len)) = lengths.chunk_info(index, chunk_idx) {
3919 ct.chunk_received(index, begin);
3920 }
3921 }
3922 }
3923
3924 self.downloaded += data.len() as u64;
3925 self.total_download += data.len() as u64 + 13; self.last_download = now_unix();
3927 self.need_save_resume = true;
3928
3929 self.verify_and_mark_piece(index).await;
3931
3932 if let Some(ref ct) = self.chunk_tracker
3934 && !ct.has_piece(index)
3935 {
3936 self.ban_web_seed(&url);
3937 return;
3938 }
3939
3940 self.assign_pieces_to_web_seeds();
3941 }
3942
3943 pub(crate) fn handle_web_seed_error(&mut self, url: String, piece: u32, message: String) {
3944 self.web_seed_in_flight.remove(&piece);
3945 warn!(%url, piece, %message, "web seed error");
3946 self.assign_pieces_to_web_seeds();
3947 }
3948
3949 fn ban_web_seed(&mut self, url: &str) {
3950 warn!(%url, "banning web seed due to hash failure");
3951 self.banned_web_seeds.insert(url.to_owned());
3952
3953 if let Some(cmd_tx) = self.web_seeds.remove(url) {
3955 let _ = cmd_tx.try_send(crate::web_seed::WebSeedCommand::Shutdown);
3956 }
3957
3958 self.web_seed_in_flight.retain(|_, v| v != url);
3960
3961 post_alert(
3962 &self.alert_tx,
3963 &self.alert_mask,
3964 AlertKind::WebSeedBanned {
3965 info_hash: self.info_hash,
3966 url: url.to_owned(),
3967 },
3968 );
3969 }
3970
3971 async fn shutdown_web_seeds(&mut self) {
3972 for (_, cmd_tx) in self.web_seeds.drain() {
3973 let _ = cmd_tx.send(crate::web_seed::WebSeedCommand::Shutdown).await;
3974 }
3975 self.web_seed_in_flight.clear();
3976 }
3977
3978 fn refresh_peer_rates(&mut self) {
3980 self.cached_peer_rates.clear();
3981 self.cached_peer_rates.reserve(self.peers.len());
3982 for (&addr, p) in &self.peers {
3983 self.cached_peer_rates.insert(addr, p.pipeline.ewma_rate());
3984 }
3985 }
3986
3987 fn update_peer_rates(&mut self) {
3990 for peer in self.peers.values_mut() {
3991 peer.download_rate = peer.download_bytes_window / 10;
3993 peer.upload_rate = peer.upload_bytes_window / 10;
3994 peer.download_bytes_window = 0;
3995 peer.upload_bytes_window = 0;
3996
3997 let target =
4006 ((peer.download_rate as f64 / 16384.0) * self.config.target_buffer_secs) as u32;
4007 let clamped = target.clamp(
4008 self.config.min_pipeline_depth,
4009 self.config.max_pipeline_depth,
4010 );
4011 peer.target_depth
4012 .store(clamped, std::sync::atomic::Ordering::Relaxed);
4013 }
4014
4015 let aggregate_download: u64 = self.peers.values().map(|p| p.download_rate).sum();
4017 if aggregate_download > self.peak_download_rate {
4018 self.peak_download_rate = aggregate_download;
4019 }
4020 }
4021
4022 async fn run_choker(&mut self) {
4023 let peer_infos: Vec<ChokerPeerInfo> = self
4024 .peers
4025 .values()
4026 .map(|p| ChokerPeerInfo {
4027 addr: p.addr,
4028 download_rate: p.download_rate,
4029 upload_rate: p.upload_rate,
4030 interested: p.peer_interested,
4031 upload_only: p.upload_only,
4032 is_seed: p.upload_only
4033 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
4034 })
4035 .collect();
4036
4037 let decision = self.choker.decide(&peer_infos);
4038
4039 for addr in &decision.to_unchoke {
4040 if let Some(peer) = self.peers.get_mut(addr)
4041 && peer.am_choking
4042 {
4043 peer.am_choking = false;
4044 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(false));
4045 }
4046 }
4047
4048 for addr in &decision.to_choke {
4049 if let Some(peer) = self.peers.get_mut(addr)
4050 && !peer.am_choking
4051 {
4052 if peer.supports_fast {
4053 let pending: Vec<(u32, u32, u32)> = peer.incoming_requests.drain(..).collect();
4054 for (index, begin, length) in pending {
4055 let _ = peer.cmd_tx.try_send(PeerCommand::RejectRequest {
4056 index,
4057 begin,
4058 length,
4059 });
4060 }
4061 }
4062 peer.am_choking = true;
4063 let _ = peer.cmd_tx.try_send(PeerCommand::SetChoking(true));
4064 }
4065 }
4066
4067 self.serve_incoming_requests().await;
4069
4070 if self.state == TorrentState::Downloading {
4075 let zombie_threshold = Duration::from_secs(30);
4076 let zombies: Vec<SocketAddr> = self
4077 .peers
4078 .values()
4079 .filter(|p| {
4080 p.bitfield.count_ones() == 0 && p.connected_at.elapsed() > zombie_threshold
4081 })
4082 .map(|p| p.addr)
4083 .collect();
4084
4085 for &addr in &zombies {
4086 debug!(%addr, "disconnecting zombie peer (empty bitfield after 30s)");
4087 self.disconnect_peer(addr, "zombie peer (empty bitfield)");
4088 }
4089 if !zombies.is_empty() {
4090 self.recalc_max_in_flight();
4091 self.mark_snapshot_dirty();
4092 }
4093 }
4094 }
4095
4096 fn rotate_optimistic(&mut self) {
4097 let peer_infos: Vec<ChokerPeerInfo> = self
4098 .peers
4099 .values()
4100 .map(|p| ChokerPeerInfo {
4101 addr: p.addr,
4102 download_rate: p.download_rate,
4103 upload_rate: p.upload_rate,
4104 interested: p.peer_interested,
4105 upload_only: p.upload_only,
4106 is_seed: p.upload_only
4107 || (self.num_pieces > 0 && p.bitfield.count_ones() == self.num_pieces),
4108 })
4109 .collect();
4110
4111 self.choker.rotate_optimistic(&peer_infos);
4112 }
4113
4114 fn handle_i2p_incoming(&mut self, stream: crate::i2p::SamStream) {
4120 if self.peers.len() >= self.effective_max_connections() {
4121 return;
4122 }
4123
4124 let synthetic_addr = self.next_i2p_synthetic_addr();
4125
4126 let remote_dest = stream.remote_destination().clone();
4127 let dest_preview = {
4128 let b64 = remote_dest.to_base64();
4129 if b64.len() >= 8 {
4130 b64[..8].to_string()
4131 } else {
4132 b64
4133 }
4134 };
4135 self.i2p_destinations.insert(synthetic_addr, remote_dest);
4136 let tcp_stream = stream.into_inner();
4137
4138 self.spawn_peer_from_stream(synthetic_addr, tcp_stream);
4139
4140 debug!(dest = %dest_preview, addr = %synthetic_addr, "accepted I2P peer");
4141 }
4142
4143 #[allow(dead_code)] fn add_i2p_peer(
4146 &mut self,
4147 dest: crate::i2p::I2pDestination,
4148 source: PeerSource,
4149 ) -> Option<SocketAddr> {
4150 if self.i2p_destinations.values().any(|d| d == &dest) {
4152 return None;
4153 }
4154 let addr = self.next_i2p_synthetic_addr();
4155 self.i2p_destinations.insert(addr, dest);
4156 if let Some(ref ps) = self.peer_states {
4158 ps.add_if_not_seen(addr, source);
4159 }
4160 Some(addr)
4161 }
4162
4163 fn next_i2p_synthetic_addr(&mut self) -> SocketAddr {
4169 self.i2p_peer_counter = self.i2p_peer_counter.wrapping_add(1);
4170 let a = ((self.i2p_peer_counter >> 16) & 0x0F) as u8 | 240;
4171 let b = ((self.i2p_peer_counter >> 8) & 0xFF) as u8;
4172 let c = (self.i2p_peer_counter & 0xFF) as u8;
4173 SocketAddr::new(
4174 std::net::IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, 1)),
4175 (self.i2p_peer_counter & 0xFFFF) as u16,
4176 )
4177 }
4178}
4179
4180pub(crate) fn is_i2p_synthetic_addr(addr: &SocketAddr) -> bool {
4182 match addr {
4183 SocketAddr::V4(v4) => v4.ip().octets()[0] & 0xF0 == 0xF0,
4184 _ => false,
4185 }
4186}
4187
4188async fn accept_incoming(
4191 listener: &mut Option<Box<dyn crate::transport::TransportListener>>,
4192) -> std::io::Result<(crate::transport::BoxedStream, SocketAddr)> {
4193 match listener {
4194 Some(l) => l.accept().await,
4195 None => std::future::pending().await,
4196 }
4197}
4198
4199async fn accept_i2p(
4202 rx: &mut Option<mpsc::Receiver<crate::i2p::SamStream>>,
4203) -> Option<crate::i2p::SamStream> {
4204 match rx {
4205 Some(rx) => rx.recv().await,
4206 None => std::future::pending().await,
4207 }
4208}
4209
4210pub(crate) fn serve_hashes(
4221 meta_v2: Option<&irontide_core::TorrentMetaV2>,
4222 version: irontide_core::TorrentVersion,
4223 lengths: Option<&Lengths>,
4224 request: &irontide_core::HashRequest,
4225) -> Option<Vec<irontide_core::Id32>> {
4226 let meta_v2 = match meta_v2 {
4228 Some(m) if version != irontide_core::TorrentVersion::V1Only => m,
4229 _ => return None,
4230 };
4231
4232 let piece_hashes = meta_v2.file_piece_hashes(&request.file_root)?;
4234
4235 let lengths = lengths?;
4237
4238 let blocks_per_piece = (meta_v2.info.piece_length / lengths.chunk_size() as u64) as u32;
4243 let num_pieces = piece_hashes.len() as u32;
4244 let num_blocks = num_pieces.saturating_mul(blocks_per_piece);
4245
4246 if !irontide_core::validate_hash_request(request, num_blocks, num_pieces) {
4247 return None;
4248 }
4249
4250 let piece_layer_base = blocks_per_piece.trailing_zeros();
4253 if request.base != piece_layer_base {
4254 return None;
4255 }
4256
4257 let start = request.index as usize;
4259 let end = (start + request.count as usize).min(piece_hashes.len());
4260 let mut hashes: Vec<irontide_core::Id32> = piece_hashes[start..end].to_vec();
4261
4262 if request.proof_layers > 0 && !piece_hashes.is_empty() {
4270 let tree = irontide_core::MerkleTree::from_leaves(&piece_hashes);
4271 let full_proof = tree.proof_path(start);
4272 let subtree_depth = if request.count > 1 {
4274 (request.count as usize)
4275 .next_power_of_two()
4276 .trailing_zeros() as usize
4277 } else {
4278 0
4279 };
4280 let available = full_proof.len().saturating_sub(subtree_depth);
4281 let proof_count = (request.proof_layers as usize).min(available);
4282 hashes.extend_from_slice(&full_proof[subtree_depth..subtree_depth + proof_count]);
4283 }
4284
4285 Some(hashes)
4286}
4287
4288#[cfg(test)]
4293mod tests {
4294 use super::*;
4295 use bytes::Bytes;
4296 use futures::{SinkExt, StreamExt};
4297 use irontide_wire::{ExtHandshake, Handshake, Message, MessageCodec};
4298 use std::time::Duration;
4299 use tokio::io::{AsyncReadExt, AsyncWriteExt};
4300 use tokio::net::TcpListener;
4301 use tokio_util::codec::{FramedRead, FramedWrite};
4302
4303 fn make_test_torrent(data: &[u8], piece_length: u64) -> TorrentMetaV1 {
4307 use serde::Serialize;
4308
4309 let mut pieces = Vec::new();
4310 let mut offset = 0;
4311 while offset < data.len() {
4312 let end = (offset + piece_length as usize).min(data.len());
4313 let hash = irontide_core::sha1(&data[offset..end]);
4314 pieces.extend_from_slice(hash.as_bytes());
4315 offset = end;
4316 }
4317
4318 #[derive(Serialize)]
4319 struct Info<'a> {
4320 length: u64,
4321 name: &'a str,
4322 #[serde(rename = "piece length")]
4323 piece_length: u64,
4324 #[serde(with = "serde_bytes")]
4325 pieces: &'a [u8],
4326 }
4327
4328 #[derive(Serialize)]
4329 struct Torrent<'a> {
4330 info: Info<'a>,
4331 }
4332
4333 let t = Torrent {
4334 info: Info {
4335 length: data.len() as u64,
4336 name: "test",
4337 piece_length,
4338 pieces: &pieces,
4339 },
4340 };
4341
4342 let bytes = irontide_bencode::to_bytes(&t).unwrap();
4343 torrent_from_bytes(&bytes).unwrap()
4344 }
4345
4346 fn test_config() -> TorrentConfig {
4347 TorrentConfig {
4348 listen_port: 0, max_peers: 200,
4350 target_request_queue: 5,
4351 download_dir: std::path::PathBuf::from("/tmp"),
4352 enable_dht: false,
4353 enable_pex: false,
4354 enable_fast: false,
4355 seed_ratio_limit: None,
4356 strict_end_game: true,
4357 upload_rate_limit: 0,
4358 download_rate_limit: 0,
4359 encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
4360 enable_utp: false,
4361 enable_web_seed: true,
4362 enable_holepunch: false,
4363 enable_bep40_eviction: true,
4364 max_web_seeds: 4,
4365 super_seeding: false,
4366 upload_only_announce: true,
4367 hashing_threads: 2,
4368 sequential_download: false,
4369 initial_picker_threshold: 4,
4370 whole_pieces_threshold: 20,
4371 snub_timeout_secs: 15,
4372 readahead_pieces: 8,
4373 streaming_timeout_escalation: true,
4374 max_concurrent_stream_reads: 8,
4375 proxy: crate::proxy::ProxyConfig::default(),
4376 anonymous_mode: false,
4377 share_mode: false,
4378 enable_i2p: false,
4379 allow_i2p_mixed: false,
4380 ssl_listen_port: 0,
4381 seed_choking_algorithm: crate::choker::SeedChokingAlgorithm::FastestUpload,
4382 choking_algorithm: crate::choker::ChokingAlgorithm::FixedSlots,
4383 piece_extent_affinity: true,
4384 suggest_mode: false,
4385 max_suggest_pieces: 10,
4386 predictive_piece_announce_ms: 0,
4387 mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
4388 auto_sequential: true,
4389 storage_mode: irontide_core::StorageMode::Auto,
4390 preallocate_mode: None,
4391 block_request_timeout_secs: 60,
4392 enable_lsd: false,
4393 force_proxy: false,
4394 steal_threshold_ratio: 10.0,
4395 steal_threshold_endgame: 3.0,
4396 min_pipeline_depth: 16,
4397 max_pipeline_depth: 512,
4398 target_buffer_secs: 2.0,
4399 peer_read_timeout_secs: 0, peer_write_timeout_secs: 0, data_contribution_timeout_secs: 0, choke_rotation_max_evictions: 0, max_concurrent_connects: 128,
4404 connect_soft_timeout: 3,
4405 url_security: crate::url_guard::UrlSecurityConfig::default(),
4406 peer_connect_timeout: 2,
4407 peer_dscp: 0x08,
4408 initial_queue_depth: 128,
4409 max_request_queue_depth: 250,
4410 request_queue_time: 3.0,
4411 max_metadata_size: 4 * 1024 * 1024,
4412 max_message_size: 16 * 1024 * 1024,
4413 max_piece_length: 32 * 1024 * 1024,
4414 max_outstanding_requests: 500,
4415 max_in_flight_pieces: 20,
4416 use_block_stealing: true,
4417 steal_stale_piece_secs: 2,
4418 fixed_pipeline_depth: 128,
4419 lock_warn_threshold_ms: 0, filesystem_direct_io: false,
4421 }
4422 }
4423
4424 fn make_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
4425 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
4426 Arc::new(MemoryStorage::new(lengths))
4427 }
4428
4429 fn make_seeded_storage(data: &[u8], piece_length: u64) -> Arc<MemoryStorage> {
4430 let lengths = Lengths::new(data.len() as u64, piece_length, DEFAULT_CHUNK_SIZE);
4431 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
4432 let num_pieces = lengths.num_pieces();
4434 for p in 0..num_pieces {
4435 let piece_size = lengths.piece_size(p) as usize;
4436 let offset = lengths.piece_offset(p) as usize;
4437 let end = offset + piece_size;
4438 storage.write_chunk(p, 0, &data[offset..end]).unwrap();
4439 }
4440 storage
4441 }
4442
4443 fn test_alert_channel() -> (broadcast::Sender<Alert>, Arc<AtomicU32>) {
4444 let (tx, _) = broadcast::channel(64);
4445 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
4446 (tx, mask)
4447 }
4448
4449 fn test_ban_manager() -> crate::session::SharedBanManager {
4450 Arc::new(parking_lot::RwLock::new(crate::ban::BanManager::new(
4451 crate::ban::BanConfig::default(),
4452 )))
4453 }
4454
4455 fn test_ip_filter() -> crate::session::SharedIpFilter {
4456 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()))
4457 }
4458
4459 fn test_disk_manager() -> (DiskManagerHandle, tokio::task::JoinHandle<()>) {
4460 DiskManagerHandle::new(crate::disk::DiskConfig::default())
4461 }
4462
4463 async fn test_register_disk(
4464 info_hash: Id20,
4465 storage: Arc<dyn TorrentStorage>,
4466 ) -> (DiskHandle, DiskManagerHandle, tokio::task::JoinHandle<()>) {
4467 let (dm, join) = test_disk_manager();
4468 let dh = dm.register_torrent(info_hash, storage).await;
4469 (dh, dm, join)
4470 }
4471
4472 const HANDSHAKE_SIZE: usize = 68;
4474
4475 #[tokio::test]
4478 async fn create_from_torrent() {
4479 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
4482 let config = test_config();
4483
4484 let (atx, amask) = test_alert_channel();
4485 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4486 let handle = TorrentHandle::from_torrent(
4487 meta,
4488 irontide_core::TorrentVersion::V1Only,
4489 None,
4490 dh,
4491 dm,
4492 config,
4493 None,
4494 None,
4495 None,
4496 None,
4497 crate::slot_tuner::SlotTuner::disabled(4),
4498 atx,
4499 amask,
4500 None,
4501 None,
4502 test_ban_manager(),
4503 test_ip_filter(),
4504 Arc::new(Vec::new()),
4505 None,
4506 None,
4507 Arc::new(crate::transport::NetworkFactory::tokio()),
4508 None, )
4510 .await
4511 .unwrap();
4512
4513 let stats = handle.stats().await.unwrap();
4514 assert_eq!(stats.state, TorrentState::Downloading);
4515 assert_eq!(stats.pieces_total, 2);
4516 assert_eq!(stats.pieces_have, 0);
4517 assert_eq!(stats.peers_connected, 0);
4518
4519 handle.shutdown().await.unwrap();
4520 }
4521
4522 #[tokio::test]
4525 async fn create_from_magnet() {
4526 let magnet = Magnet {
4527 info_hashes: irontide_core::InfoHashes::v1_only(
4528 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
4529 ),
4530 display_name: Some("test".into()),
4531 trackers: vec![],
4532 peers: vec![],
4533 selected_files: None,
4534 };
4535 let config = test_config();
4536
4537 let (atx, amask) = test_alert_channel();
4538 let (dm, _dj) = test_disk_manager();
4539 let handle = TorrentHandle::from_magnet(
4540 magnet,
4541 dm,
4542 config,
4543 None,
4544 None,
4545 None,
4546 None,
4547 crate::slot_tuner::SlotTuner::disabled(4),
4548 atx,
4549 amask,
4550 None,
4551 None,
4552 test_ban_manager(),
4553 test_ip_filter(),
4554 Arc::new(Vec::new()),
4555 None,
4556 None,
4557 Arc::new(crate::transport::NetworkFactory::tokio()),
4558 None, )
4560 .await
4561 .unwrap();
4562
4563 let stats = handle.stats().await.unwrap();
4564 assert_eq!(stats.state, TorrentState::FetchingMetadata);
4565 assert_eq!(stats.pieces_total, 0);
4566
4567 handle.shutdown().await.unwrap();
4568 }
4569
4570 #[tokio::test]
4573 async fn add_peers_increases_available() {
4574 let data = vec![0xAB; 32768];
4575 let meta = make_test_torrent(&data, 16384);
4576 let storage = make_storage(&data, 16384);
4577 let config = test_config();
4578
4579 let (atx, amask) = test_alert_channel();
4580 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4581 let handle = TorrentHandle::from_torrent(
4582 meta,
4583 irontide_core::TorrentVersion::V1Only,
4584 None,
4585 dh,
4586 dm,
4587 config,
4588 None,
4589 None,
4590 None,
4591 None,
4592 crate::slot_tuner::SlotTuner::disabled(4),
4593 atx,
4594 amask,
4595 None,
4596 None,
4597 test_ban_manager(),
4598 test_ip_filter(),
4599 Arc::new(Vec::new()),
4600 None,
4601 None,
4602 Arc::new(crate::transport::NetworkFactory::tokio()),
4603 None, )
4605 .await
4606 .unwrap();
4607
4608 let _listener1 = TcpListener::bind("127.0.0.1:0").await.unwrap();
4610 let addr1 = _listener1.local_addr().unwrap();
4611 let _listener2 = TcpListener::bind("127.0.0.1:0").await.unwrap();
4612 let addr2 = _listener2.local_addr().unwrap();
4613
4614 handle
4615 .add_peers(vec![addr1, addr2], PeerSource::Tracker)
4616 .await
4617 .unwrap();
4618
4619 tokio::time::sleep(Duration::from_millis(100)).await;
4621
4622 let stats = handle.stats().await.unwrap();
4623 assert!(
4625 stats.peers_available + stats.peers_connected >= 2,
4626 "expected at least 2 peers known, got available={}, connected={}",
4627 stats.peers_available,
4628 stats.peers_connected
4629 );
4630
4631 handle.shutdown().await.unwrap();
4632 }
4633
4634 #[tokio::test]
4637 async fn stats_reporting() {
4638 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
4641 let config = test_config();
4642
4643 let (atx, amask) = test_alert_channel();
4644 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4645 let handle = TorrentHandle::from_torrent(
4646 meta,
4647 irontide_core::TorrentVersion::V1Only,
4648 None,
4649 dh,
4650 dm,
4651 config,
4652 None,
4653 None,
4654 None,
4655 None,
4656 crate::slot_tuner::SlotTuner::disabled(4),
4657 atx,
4658 amask,
4659 None,
4660 None,
4661 test_ban_manager(),
4662 test_ip_filter(),
4663 Arc::new(Vec::new()),
4664 None,
4665 None,
4666 Arc::new(crate::transport::NetworkFactory::tokio()),
4667 None, )
4669 .await
4670 .unwrap();
4671
4672 let stats = handle.stats().await.unwrap();
4673 assert_eq!(stats.state, TorrentState::Downloading);
4674 assert_eq!(stats.downloaded, 0);
4675 assert_eq!(stats.uploaded, 0);
4676 assert_eq!(stats.pieces_have, 0);
4677 assert_eq!(stats.pieces_total, 4);
4678 assert_eq!(stats.peers_connected, 0);
4679 assert_eq!(stats.peers_available, 0);
4680
4681 handle.shutdown().await.unwrap();
4682 }
4683
4684 #[tokio::test]
4687 async fn private_torrent_disables_dht_pex() {
4688 use serde::Serialize;
4690
4691 let data = vec![0xAB; 16384];
4692 let hash = irontide_core::sha1(&data);
4693 let mut pieces = Vec::new();
4694 pieces.extend_from_slice(hash.as_bytes());
4695
4696 #[derive(Serialize)]
4697 struct Info<'a> {
4698 length: u64,
4699 name: &'a str,
4700 #[serde(rename = "piece length")]
4701 piece_length: u64,
4702 #[serde(with = "serde_bytes")]
4703 pieces: &'a [u8],
4704 private: i64,
4705 }
4706
4707 #[derive(Serialize)]
4708 struct Torrent<'a> {
4709 info: Info<'a>,
4710 }
4711
4712 let t = Torrent {
4713 info: Info {
4714 length: data.len() as u64,
4715 name: "private_test",
4716 piece_length: 16384,
4717 pieces: &pieces,
4718 private: 1,
4719 },
4720 };
4721
4722 let bytes = irontide_bencode::to_bytes(&t).unwrap();
4723 let meta = torrent_from_bytes(&bytes).unwrap();
4724 assert_eq!(meta.info.private, Some(1));
4725
4726 let storage = make_storage(&data, 16384);
4727 let mut config = test_config();
4728 config.enable_dht = true;
4729 config.enable_pex = true;
4730
4731 let (atx, amask) = test_alert_channel();
4733 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4734 let handle = TorrentHandle::from_torrent(
4735 meta,
4736 irontide_core::TorrentVersion::V1Only,
4737 None,
4738 dh,
4739 dm,
4740 config,
4741 None,
4742 None,
4743 None,
4744 None,
4745 crate::slot_tuner::SlotTuner::disabled(4),
4746 atx,
4747 amask,
4748 None,
4749 None,
4750 test_ban_manager(),
4751 test_ip_filter(),
4752 Arc::new(Vec::new()),
4753 None,
4754 None,
4755 Arc::new(crate::transport::NetworkFactory::tokio()),
4756 None, )
4758 .await
4759 .unwrap();
4760
4761 let stats = handle.stats().await.unwrap();
4765 assert_eq!(stats.state, TorrentState::Downloading);
4766
4767 handle.shutdown().await.unwrap();
4768 }
4769
4770 #[tokio::test]
4773 async fn shutdown_cleanup() {
4774 let data = vec![0xAB; 16384];
4775 let meta = make_test_torrent(&data, 16384);
4776 let storage = make_storage(&data, 16384);
4777 let config = test_config();
4778
4779 let (atx, amask) = test_alert_channel();
4780 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4781 let handle = TorrentHandle::from_torrent(
4782 meta,
4783 irontide_core::TorrentVersion::V1Only,
4784 None,
4785 dh,
4786 dm,
4787 config,
4788 None,
4789 None,
4790 None,
4791 None,
4792 crate::slot_tuner::SlotTuner::disabled(4),
4793 atx,
4794 amask,
4795 None,
4796 None,
4797 test_ban_manager(),
4798 test_ip_filter(),
4799 Arc::new(Vec::new()),
4800 None,
4801 None,
4802 Arc::new(crate::transport::NetworkFactory::tokio()),
4803 None, )
4805 .await
4806 .unwrap();
4807
4808 handle.shutdown().await.unwrap();
4809
4810 tokio::time::sleep(Duration::from_millis(50)).await;
4812 let result = handle.stats().await;
4813 assert!(result.is_err());
4814 }
4815
4816 #[tokio::test]
4819 async fn duplicate_peers_ignored() {
4820 let data = vec![0xAB; 16384];
4821 let meta = make_test_torrent(&data, 16384);
4822 let storage = make_storage(&data, 16384);
4823 let config = test_config();
4824
4825 let (atx, amask) = test_alert_channel();
4826 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4827 let handle = TorrentHandle::from_torrent(
4828 meta,
4829 irontide_core::TorrentVersion::V1Only,
4830 None,
4831 dh,
4832 dm,
4833 config,
4834 None,
4835 None,
4836 None,
4837 None,
4838 crate::slot_tuner::SlotTuner::disabled(4),
4839 atx,
4840 amask,
4841 None,
4842 None,
4843 test_ban_manager(),
4844 test_ip_filter(),
4845 Arc::new(Vec::new()),
4846 None,
4847 None,
4848 Arc::new(crate::transport::NetworkFactory::tokio()),
4849 None, )
4851 .await
4852 .unwrap();
4853
4854 let _listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4856 let addr = _listener.local_addr().unwrap();
4857 handle
4858 .add_peers(vec![addr, addr, addr], PeerSource::Tracker)
4859 .await
4860 .unwrap();
4861
4862 tokio::time::sleep(Duration::from_millis(100)).await;
4863 let stats = handle.stats().await.unwrap();
4864 assert!(
4866 stats.peers_available + stats.peers_connected <= 1,
4867 "expected at most 1 unique peer, got available={}, connected={}",
4868 stats.peers_available,
4869 stats.peers_connected
4870 );
4871
4872 handle.shutdown().await.unwrap();
4873 }
4874
4875 #[tokio::test]
4878 async fn cloned_handle_shares_actor() {
4879 let data = vec![0xAB; 16384];
4880 let meta = make_test_torrent(&data, 16384);
4881 let storage = make_storage(&data, 16384);
4882 let config = test_config();
4883
4884 let (atx, amask) = test_alert_channel();
4885 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4886 let handle = TorrentHandle::from_torrent(
4887 meta,
4888 irontide_core::TorrentVersion::V1Only,
4889 None,
4890 dh,
4891 dm,
4892 config,
4893 None,
4894 None,
4895 None,
4896 None,
4897 crate::slot_tuner::SlotTuner::disabled(4),
4898 atx,
4899 amask,
4900 None,
4901 None,
4902 test_ban_manager(),
4903 test_ip_filter(),
4904 Arc::new(Vec::new()),
4905 None,
4906 None,
4907 Arc::new(crate::transport::NetworkFactory::tokio()),
4908 None, )
4910 .await
4911 .unwrap();
4912 let handle2 = handle.clone();
4913
4914 let _listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4916 let peer_addr = _listener.local_addr().unwrap();
4917
4918 handle
4920 .add_peers(vec![peer_addr], PeerSource::Tracker)
4921 .await
4922 .unwrap();
4923
4924 tokio::time::sleep(Duration::from_millis(100)).await;
4925
4926 let stats = handle2.stats().await.unwrap();
4928 assert!(
4929 stats.peers_available + stats.peers_connected >= 1,
4930 "expected at least 1 peer known, got available={}, connected={}",
4931 stats.peers_available,
4932 stats.peers_connected
4933 );
4934
4935 handle.shutdown().await.unwrap();
4936 }
4937
4938 #[tokio::test]
4941 async fn peer_connect_and_disconnect_via_listener() {
4942 let data = vec![0xAB; 16384];
4943 let meta = make_test_torrent(&data, 16384);
4944 let info_hash = meta.info_hash;
4945 let storage = make_storage(&data, 16384);
4946
4947 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
4949 let listen_addr = listener.local_addr().unwrap();
4950
4951 let config = TorrentConfig {
4952 listen_port: listen_addr.port(),
4953 ..test_config()
4954 };
4955
4956 drop(listener);
4958
4959 let (atx, amask) = test_alert_channel();
4960 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
4961 let handle = TorrentHandle::from_torrent(
4962 meta,
4963 irontide_core::TorrentVersion::V1Only,
4964 None,
4965 dh,
4966 dm,
4967 config,
4968 None,
4969 None,
4970 None,
4971 None,
4972 crate::slot_tuner::SlotTuner::disabled(4),
4973 atx,
4974 amask,
4975 None,
4976 None,
4977 test_ban_manager(),
4978 test_ip_filter(),
4979 Arc::new(Vec::new()),
4980 None,
4981 None,
4982 Arc::new(crate::transport::NetworkFactory::tokio()),
4983 None, )
4985 .await
4986 .unwrap();
4987
4988 tokio::time::sleep(Duration::from_millis(50)).await;
4990
4991 let mut stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
4993
4994 let remote_id = Id20::from_hex("1111111111111111111111111111111111111111").unwrap();
4996 let remote_hs = Handshake::new(info_hash, remote_id);
4997 stream.write_all(&remote_hs.to_bytes()).await.unwrap();
4998 stream.flush().await.unwrap();
4999
5000 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
5001 stream.read_exact(&mut hs_buf).await.unwrap();
5002 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
5003 assert_eq!(their_hs.info_hash, info_hash);
5004
5005 tokio::time::sleep(Duration::from_millis(100)).await;
5007
5008 let stats = handle.stats().await.unwrap();
5009 assert_eq!(stats.peers_connected, 1);
5010
5011 drop(stream);
5013
5014 tokio::time::sleep(Duration::from_millis(200)).await;
5016
5017 let stats = handle.stats().await.unwrap();
5018 assert_eq!(stats.peers_connected, 0);
5019
5020 handle.shutdown().await.unwrap();
5021 }
5022
5023 #[tokio::test]
5029 async fn piece_download_and_verify() {
5030 let data = vec![0xCDu8; 16384];
5032 let meta = make_test_torrent(&data, 16384);
5033 let info_hash = meta.info_hash;
5034 let storage = make_storage(&data, 16384);
5035
5036 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5037 let listen_addr = listener.local_addr().unwrap();
5038 drop(listener);
5039
5040 let config = TorrentConfig {
5041 listen_port: listen_addr.port(),
5042 ..test_config()
5043 };
5044
5045 let (atx, amask) = test_alert_channel();
5046 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5047 let handle = TorrentHandle::from_torrent(
5048 meta,
5049 irontide_core::TorrentVersion::V1Only,
5050 None,
5051 dh,
5052 dm,
5053 config,
5054 None,
5055 None,
5056 None,
5057 None,
5058 crate::slot_tuner::SlotTuner::disabled(4),
5059 atx,
5060 amask,
5061 None,
5062 None,
5063 test_ban_manager(),
5064 test_ip_filter(),
5065 Arc::new(Vec::new()),
5066 None,
5067 None,
5068 Arc::new(crate::transport::NetworkFactory::tokio()),
5069 None, )
5071 .await
5072 .unwrap();
5073
5074 tokio::time::sleep(Duration::from_millis(50)).await;
5075
5076 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
5078 let remote_id = Id20::from_hex("2222222222222222222222222222222222222222").unwrap();
5079
5080 let mock_data = data.clone();
5082 let mock_task = tokio::spawn(async move {
5083 let (reader, writer) = tokio::io::split(stream);
5084 let mut reader = reader;
5085 let mut writer = writer;
5086
5087 let hs = Handshake::new(info_hash, remote_id);
5089 writer.write_all(&hs.to_bytes()).await.unwrap();
5090 writer.flush().await.unwrap();
5091
5092 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
5093 reader.read_exact(&mut hs_buf).await.unwrap();
5094
5095 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
5097 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
5098
5099 let _msg = framed_read.next().await;
5101
5102 let ext_hs = ExtHandshake::new();
5104 let payload = ext_hs.to_bytes().unwrap();
5105 framed_write
5106 .send(Message::Extended { ext_id: 0, payload })
5107 .await
5108 .unwrap();
5109
5110 let mut bf = Bitfield::new(1);
5112 bf.set(0);
5113 framed_write
5114 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
5115 .await
5116 .unwrap();
5117
5118 framed_write.send(Message::Unchoke).await.unwrap();
5120
5121 while let Some(Ok(msg)) = framed_read.next().await {
5123 match msg {
5124 Message::Request {
5125 index,
5126 begin,
5127 length,
5128 } => {
5129 let start = begin as usize;
5130 let end = start + length as usize;
5131 let piece_data = &mock_data[start..end];
5132 framed_write
5133 .send(Message::Piece {
5134 index,
5135 begin,
5136 data_0: Bytes::copy_from_slice(piece_data),
5137 data_1: Bytes::new(),
5138 })
5139 .await
5140 .unwrap();
5141 }
5142 Message::Interested => {
5143 }
5145 _ => {}
5146 }
5147 }
5148 });
5149
5150 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
5152 loop {
5153 tokio::time::sleep(Duration::from_millis(100)).await;
5154 let stats = handle.stats().await.unwrap();
5155 if stats.state == TorrentState::Seeding {
5156 assert_eq!(stats.pieces_have, 1);
5157 assert_eq!(stats.pieces_total, 1);
5158 break;
5159 }
5160 if tokio::time::Instant::now() > deadline {
5161 let stats = handle.stats().await.unwrap();
5162 panic!(
5163 "download did not complete within 5s, state={:?}, have={}/{}",
5164 stats.state, stats.pieces_have, stats.pieces_total
5165 );
5166 }
5167 }
5168
5169 handle.shutdown().await.unwrap();
5170 mock_task.abort();
5171 }
5172
5173 #[tokio::test]
5176 async fn failed_piece_verification() {
5177 let data = vec![0xEEu8; 16384];
5179 let meta = make_test_torrent(&data, 16384);
5180 let info_hash = meta.info_hash;
5181 let storage = make_storage(&data, 16384);
5182
5183 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5184 let listen_addr = listener.local_addr().unwrap();
5185 drop(listener);
5186
5187 let config = TorrentConfig {
5188 listen_port: listen_addr.port(),
5189 ..test_config()
5190 };
5191
5192 let (atx, amask) = test_alert_channel();
5193 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5194 let handle = TorrentHandle::from_torrent(
5195 meta,
5196 irontide_core::TorrentVersion::V1Only,
5197 None,
5198 dh,
5199 dm,
5200 config,
5201 None,
5202 None,
5203 None,
5204 None,
5205 crate::slot_tuner::SlotTuner::disabled(4),
5206 atx,
5207 amask,
5208 None,
5209 None,
5210 test_ban_manager(),
5211 test_ip_filter(),
5212 Arc::new(Vec::new()),
5213 None,
5214 None,
5215 Arc::new(crate::transport::NetworkFactory::tokio()),
5216 None, )
5218 .await
5219 .unwrap();
5220
5221 tokio::time::sleep(Duration::from_millis(50)).await;
5222
5223 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
5225 let remote_id = Id20::from_hex("3333333333333333333333333333333333333333").unwrap();
5226
5227 let correct_data = data.clone();
5228 let mock_task = tokio::spawn(async move {
5229 let (reader, writer) = tokio::io::split(stream);
5230
5231 let mut writer = writer;
5233 let mut reader = reader;
5234 let hs = Handshake::new(info_hash, remote_id);
5235 writer.write_all(&hs.to_bytes()).await.unwrap();
5236 writer.flush().await.unwrap();
5237
5238 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
5239 reader.read_exact(&mut hs_buf).await.unwrap();
5240
5241 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
5242 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
5243
5244 let _msg = framed_read.next().await;
5246
5247 let ext_hs = ExtHandshake::new();
5249 let payload = ext_hs.to_bytes().unwrap();
5250 framed_write
5251 .send(Message::Extended { ext_id: 0, payload })
5252 .await
5253 .unwrap();
5254
5255 let mut bf = Bitfield::new(1);
5257 bf.set(0);
5258 framed_write
5259 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
5260 .await
5261 .unwrap();
5262
5263 framed_write.send(Message::Unchoke).await.unwrap();
5265
5266 let mut request_count = 0u32;
5267 while let Some(Ok(msg)) = framed_read.next().await {
5268 match msg {
5269 Message::Request {
5270 index,
5271 begin,
5272 length,
5273 } => {
5274 request_count += 1;
5275 let piece_data = if request_count <= 1 {
5276 vec![0xFF; length as usize]
5278 } else {
5279 let start = begin as usize;
5281 let end = start + length as usize;
5282 correct_data[start..end].to_vec()
5283 };
5284 framed_write
5285 .send(Message::Piece {
5286 index,
5287 begin,
5288 data_0: Bytes::from(piece_data),
5289 data_1: Bytes::new(),
5290 })
5291 .await
5292 .unwrap();
5293 }
5294 Message::Interested => {}
5295 _ => {}
5296 }
5297 }
5298 });
5299
5300 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
5302 loop {
5303 tokio::time::sleep(Duration::from_millis(100)).await;
5304 let stats = handle.stats().await.unwrap();
5305 if stats.state == TorrentState::Seeding {
5306 assert_eq!(stats.pieces_have, 1);
5307 break;
5308 }
5309 if tokio::time::Instant::now() > deadline {
5310 let stats = handle.stats().await.unwrap();
5311 panic!(
5312 "download did not complete after retry within 5s, state={:?}, have={}",
5313 stats.state, stats.pieces_have,
5314 );
5315 }
5316 }
5317
5318 handle.shutdown().await.unwrap();
5319 mock_task.abort();
5320 }
5321
5322 #[tokio::test]
5325 async fn complete_transitions_state() {
5326 let data = vec![0xBBu8; 32768];
5328 let meta = make_test_torrent(&data, 16384);
5329 let info_hash = meta.info_hash;
5330 let storage = make_storage(&data, 16384);
5331
5332 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5333 let listen_addr = listener.local_addr().unwrap();
5334 drop(listener);
5335
5336 let config = TorrentConfig {
5337 listen_port: listen_addr.port(),
5338 ..test_config()
5339 };
5340
5341 let (atx, amask) = test_alert_channel();
5342 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5343 let handle = TorrentHandle::from_torrent(
5344 meta,
5345 irontide_core::TorrentVersion::V1Only,
5346 None,
5347 dh,
5348 dm,
5349 config,
5350 None,
5351 None,
5352 None,
5353 None,
5354 crate::slot_tuner::SlotTuner::disabled(4),
5355 atx,
5356 amask,
5357 None,
5358 None,
5359 test_ban_manager(),
5360 test_ip_filter(),
5361 Arc::new(Vec::new()),
5362 None,
5363 None,
5364 Arc::new(crate::transport::NetworkFactory::tokio()),
5365 None, )
5367 .await
5368 .unwrap();
5369
5370 tokio::time::sleep(Duration::from_millis(50)).await;
5371
5372 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
5374 let remote_id = Id20::from_hex("4444444444444444444444444444444444444444").unwrap();
5375
5376 let mock_data = data.clone();
5377 let mock_task = tokio::spawn(async move {
5378 let (reader, writer) = tokio::io::split(stream);
5379 let mut writer = writer;
5380 let mut reader = reader;
5381
5382 let hs = Handshake::new(info_hash, remote_id);
5383 writer.write_all(&hs.to_bytes()).await.unwrap();
5384 writer.flush().await.unwrap();
5385
5386 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
5387 reader.read_exact(&mut hs_buf).await.unwrap();
5388
5389 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
5390 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
5391
5392 let _msg = framed_read.next().await;
5394
5395 let ext_hs = ExtHandshake::new();
5397 let payload = ext_hs.to_bytes().unwrap();
5398 framed_write
5399 .send(Message::Extended { ext_id: 0, payload })
5400 .await
5401 .unwrap();
5402
5403 let mut bf = Bitfield::new(2);
5405 bf.set(0);
5406 bf.set(1);
5407 framed_write
5408 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
5409 .await
5410 .unwrap();
5411
5412 framed_write.send(Message::Unchoke).await.unwrap();
5413
5414 while let Some(Ok(msg)) = framed_read.next().await {
5415 match msg {
5416 Message::Request {
5417 index,
5418 begin,
5419 length,
5420 } => {
5421 let abs_start = (index as usize * 16384) + begin as usize;
5422 let abs_end = abs_start + length as usize;
5423 let piece_data = &mock_data[abs_start..abs_end];
5424 framed_write
5425 .send(Message::Piece {
5426 index,
5427 begin,
5428 data_0: Bytes::copy_from_slice(piece_data),
5429 data_1: Bytes::new(),
5430 })
5431 .await
5432 .unwrap();
5433 }
5434 Message::Interested => {}
5435 _ => {}
5436 }
5437 }
5438 });
5439
5440 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
5441 loop {
5442 tokio::time::sleep(Duration::from_millis(100)).await;
5443 let stats = handle.stats().await.unwrap();
5444 if stats.state == TorrentState::Seeding {
5445 assert_eq!(stats.pieces_have, 2);
5446 assert_eq!(stats.pieces_total, 2);
5447 break;
5448 }
5449 if tokio::time::Instant::now() > deadline {
5450 let stats = handle.stats().await.unwrap();
5451 panic!(
5452 "expected Complete, got {:?}, have={}/{}",
5453 stats.state, stats.pieces_have, stats.pieces_total
5454 );
5455 }
5456 }
5457
5458 handle.shutdown().await.unwrap();
5459 mock_task.abort();
5460 }
5461
5462 #[tokio::test]
5465 async fn multi_chunk_piece_download() {
5466 let data = vec![0xAAu8; 32768];
5468 let meta = make_test_torrent(&data, 32768);
5469 let info_hash = meta.info_hash;
5470 let storage = make_storage(&data, 32768);
5471
5472 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5473 let listen_addr = listener.local_addr().unwrap();
5474 drop(listener);
5475
5476 let config = TorrentConfig {
5477 listen_port: listen_addr.port(),
5478 ..test_config()
5479 };
5480
5481 let (atx, amask) = test_alert_channel();
5482 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5483 let handle = TorrentHandle::from_torrent(
5484 meta,
5485 irontide_core::TorrentVersion::V1Only,
5486 None,
5487 dh,
5488 dm,
5489 config,
5490 None,
5491 None,
5492 None,
5493 None,
5494 crate::slot_tuner::SlotTuner::disabled(4),
5495 atx,
5496 amask,
5497 None,
5498 None,
5499 test_ban_manager(),
5500 test_ip_filter(),
5501 Arc::new(Vec::new()),
5502 None,
5503 None,
5504 Arc::new(crate::transport::NetworkFactory::tokio()),
5505 None, )
5507 .await
5508 .unwrap();
5509
5510 tokio::time::sleep(Duration::from_millis(50)).await;
5511
5512 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
5513 let remote_id = Id20::from_hex("5555555555555555555555555555555555555555").unwrap();
5514
5515 let mock_data = data.clone();
5516 let mock_task = tokio::spawn(async move {
5517 let (reader, writer) = tokio::io::split(stream);
5518 let mut writer = writer;
5519 let mut reader = reader;
5520
5521 let hs = Handshake::new(info_hash, remote_id);
5522 writer.write_all(&hs.to_bytes()).await.unwrap();
5523 writer.flush().await.unwrap();
5524
5525 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
5526 reader.read_exact(&mut hs_buf).await.unwrap();
5527
5528 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
5529 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
5530
5531 let _msg = framed_read.next().await;
5532
5533 let ext_hs = ExtHandshake::new();
5534 let payload = ext_hs.to_bytes().unwrap();
5535 framed_write
5536 .send(Message::Extended { ext_id: 0, payload })
5537 .await
5538 .unwrap();
5539
5540 let mut bf = Bitfield::new(1);
5541 bf.set(0);
5542 framed_write
5543 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
5544 .await
5545 .unwrap();
5546
5547 framed_write.send(Message::Unchoke).await.unwrap();
5548
5549 while let Some(Ok(msg)) = framed_read.next().await {
5550 match msg {
5551 Message::Request {
5552 index: _,
5553 begin,
5554 length,
5555 } => {
5556 let start = begin as usize;
5557 let end = start + length as usize;
5558 framed_write
5559 .send(Message::Piece {
5560 index: 0,
5561 begin,
5562 data_0: Bytes::copy_from_slice(&mock_data[start..end]),
5563 data_1: Bytes::new(),
5564 })
5565 .await
5566 .unwrap();
5567 }
5568 Message::Interested => {}
5569 _ => {}
5570 }
5571 }
5572 });
5573
5574 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
5575 loop {
5576 tokio::time::sleep(Duration::from_millis(100)).await;
5577 let stats = handle.stats().await.unwrap();
5578 if stats.state == TorrentState::Seeding {
5579 assert_eq!(stats.pieces_have, 1);
5580 break;
5581 }
5582 if tokio::time::Instant::now() > deadline {
5583 panic!("multi-chunk download did not complete within 5s");
5584 }
5585 }
5586
5587 handle.shutdown().await.unwrap();
5588 mock_task.abort();
5589 }
5590
5591 #[tokio::test]
5594 async fn seeder_leecher_integration() {
5595 let data = vec![0xDDu8; 32768]; let piece_length = 16384u64;
5598 let meta = make_test_torrent(&data, piece_length);
5599 let info_hash = meta.info_hash;
5600
5601 let seeder_storage = make_seeded_storage(&data, piece_length);
5603
5604 let seeder_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
5610 let seeder_addr = seeder_listener.local_addr().unwrap();
5611
5612 let seeder_task = tokio::spawn(async move {
5613 let (stream, _addr) = seeder_listener.accept().await.unwrap();
5614 let (reader, writer) = tokio::io::split(stream);
5615 let mut writer = writer;
5616 let mut reader = reader;
5617
5618 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
5620 reader.read_exact(&mut hs_buf).await.unwrap();
5621 let their_hs = Handshake::from_bytes(&hs_buf).unwrap();
5622 assert_eq!(their_hs.info_hash, info_hash);
5623
5624 let hs = Handshake::new(info_hash, PeerId::generate().0);
5625 writer.write_all(&hs.to_bytes()).await.unwrap();
5626 writer.flush().await.unwrap();
5627
5628 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
5629 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
5630
5631 let _msg = framed_read.next().await;
5633
5634 let ext_hs = ExtHandshake::new();
5636 let payload = ext_hs.to_bytes().unwrap();
5637 framed_write
5638 .send(Message::Extended { ext_id: 0, payload })
5639 .await
5640 .unwrap();
5641
5642 let mut bf = Bitfield::new(2);
5644 bf.set(0);
5645 bf.set(1);
5646 framed_write
5647 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
5648 .await
5649 .unwrap();
5650
5651 framed_write.send(Message::Unchoke).await.unwrap();
5653
5654 while let Some(Ok(msg)) = framed_read.next().await {
5656 match msg {
5657 Message::Request {
5658 index,
5659 begin,
5660 length,
5661 } => {
5662 let piece_data = seeder_storage.read_chunk(index, begin, length).unwrap();
5663 framed_write
5664 .send(Message::Piece {
5665 index,
5666 begin,
5667 data_0: Bytes::from(piece_data),
5668 data_1: Bytes::new(),
5669 })
5670 .await
5671 .unwrap();
5672 }
5673 Message::Interested => {}
5674 _ => {}
5675 }
5676 }
5677 });
5678
5679 let leecher_storage = make_storage(&data, piece_length);
5681 let leecher_meta = make_test_torrent(&data, piece_length);
5682
5683 let leecher_config = test_config();
5684 let (latx, lamask) = test_alert_channel();
5685 let (ldh, ldm, _ldj) = test_register_disk(leecher_meta.info_hash, leecher_storage).await;
5686 let leecher = TorrentHandle::from_torrent(
5687 leecher_meta,
5688 irontide_core::TorrentVersion::V1Only,
5689 None,
5690 ldh,
5691 ldm,
5692 leecher_config,
5693 None,
5694 None,
5695 None,
5696 None,
5697 crate::slot_tuner::SlotTuner::disabled(4),
5698 latx,
5699 lamask,
5700 None,
5701 None,
5702 test_ban_manager(),
5703 test_ip_filter(),
5704 Arc::new(Vec::new()),
5705 None,
5706 None,
5707 Arc::new(crate::transport::NetworkFactory::tokio()),
5708 None, )
5710 .await
5711 .unwrap();
5712
5713 leecher
5715 .add_peers(vec![seeder_addr], PeerSource::Tracker)
5716 .await
5717 .unwrap();
5718
5719 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
5723 loop {
5724 tokio::time::sleep(Duration::from_millis(200)).await;
5725 let stats = leecher.stats().await.unwrap();
5726 if stats.state == TorrentState::Seeding {
5727 assert_eq!(stats.pieces_have, 2);
5728 assert_eq!(stats.pieces_total, 2);
5729 break;
5730 }
5731 if tokio::time::Instant::now() > deadline {
5732 let stats = leecher.stats().await.unwrap();
5733 panic!(
5734 "seeder/leecher: leecher did not complete, state={:?}, have={}/{}, connected={}, available={}",
5735 stats.state,
5736 stats.pieces_have,
5737 stats.pieces_total,
5738 stats.peers_connected,
5739 stats.peers_available,
5740 );
5741 }
5742 }
5743
5744 leecher.shutdown().await.unwrap();
5745 seeder_task.abort();
5746 }
5747
5748 #[tokio::test]
5751 async fn magnet_initial_stats() {
5752 let magnet = Magnet {
5753 info_hashes: irontide_core::InfoHashes::v1_only(
5754 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
5755 ),
5756 display_name: Some("magnet test".into()),
5757 trackers: vec![],
5758 peers: vec![],
5759 selected_files: None,
5760 };
5761
5762 let (atx, amask) = test_alert_channel();
5763 let (dm, _dj) = test_disk_manager();
5764 let handle = TorrentHandle::from_magnet(
5765 magnet,
5766 dm,
5767 test_config(),
5768 None,
5769 None,
5770 None,
5771 None,
5772 crate::slot_tuner::SlotTuner::disabled(4),
5773 atx,
5774 amask,
5775 None,
5776 None,
5777 test_ban_manager(),
5778 test_ip_filter(),
5779 Arc::new(Vec::new()),
5780 None,
5781 None,
5782 Arc::new(crate::transport::NetworkFactory::tokio()),
5783 None, )
5785 .await
5786 .unwrap();
5787
5788 let stats = handle.stats().await.unwrap();
5789 assert_eq!(stats.state, TorrentState::FetchingMetadata);
5790 assert_eq!(stats.pieces_total, 0);
5791 assert_eq!(stats.pieces_have, 0);
5792 assert_eq!(stats.downloaded, 0);
5793 assert_eq!(stats.uploaded, 0);
5794 assert_eq!(stats.peers_connected, 0);
5795 assert_eq!(stats.peers_available, 0);
5796
5797 handle.shutdown().await.unwrap();
5798 }
5799
5800 #[tokio::test]
5803 async fn tracker_populated_from_metadata() {
5804 use serde::Serialize;
5805
5806 let data = vec![0xAB; 16384];
5807 let hash = irontide_core::sha1(&data);
5808 let mut pieces = Vec::new();
5809 pieces.extend_from_slice(hash.as_bytes());
5810
5811 #[derive(Serialize)]
5812 struct Info<'a> {
5813 length: u64,
5814 name: &'a str,
5815 #[serde(rename = "piece length")]
5816 piece_length: u64,
5817 #[serde(with = "serde_bytes")]
5818 pieces: &'a [u8],
5819 }
5820
5821 #[derive(Serialize)]
5822 struct Torrent<'a> {
5823 announce: &'a str,
5824 info: Info<'a>,
5825 }
5826
5827 let t = Torrent {
5828 announce: "http://tracker.example.com:8080/announce",
5829 info: Info {
5830 length: data.len() as u64,
5831 name: "test",
5832 piece_length: 16384,
5833 pieces: &pieces,
5834 },
5835 };
5836
5837 let bytes = irontide_bencode::to_bytes(&t).unwrap();
5838 let meta = torrent_from_bytes(&bytes).unwrap();
5839 assert!(meta.announce.is_some());
5840
5841 let storage = make_storage(&data, 16384);
5842 let config = test_config();
5843
5844 let (atx, amask) = test_alert_channel();
5847 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5848 let handle = TorrentHandle::from_torrent(
5849 meta,
5850 irontide_core::TorrentVersion::V1Only,
5851 None,
5852 dh,
5853 dm,
5854 config,
5855 None,
5856 None,
5857 None,
5858 None,
5859 crate::slot_tuner::SlotTuner::disabled(4),
5860 atx,
5861 amask,
5862 None,
5863 None,
5864 test_ban_manager(),
5865 test_ip_filter(),
5866 Arc::new(Vec::new()),
5867 None,
5868 None,
5869 Arc::new(crate::transport::NetworkFactory::tokio()),
5870 None, )
5872 .await
5873 .unwrap();
5874
5875 let stats = handle.stats().await.unwrap();
5876 assert_eq!(stats.state, TorrentState::Downloading);
5877
5878 handle.shutdown().await.unwrap();
5879 }
5880
5881 #[tokio::test]
5884 async fn private_torrent_no_dht_field() {
5885 let data = vec![0xAB; 16384];
5886 let hash = irontide_core::sha1(&data);
5887 let mut pieces = Vec::new();
5888 pieces.extend_from_slice(hash.as_bytes());
5889
5890 use serde::Serialize;
5891
5892 #[derive(Serialize)]
5893 struct Info<'a> {
5894 length: u64,
5895 name: &'a str,
5896 #[serde(rename = "piece length")]
5897 piece_length: u64,
5898 #[serde(with = "serde_bytes")]
5899 pieces: &'a [u8],
5900 private: i64,
5901 }
5902
5903 #[derive(Serialize)]
5904 struct Torrent<'a> {
5905 announce: &'a str,
5906 info: Info<'a>,
5907 }
5908
5909 let t = Torrent {
5910 announce: "http://private-tracker.example.com/announce",
5911 info: Info {
5912 length: data.len() as u64,
5913 name: "private_test",
5914 piece_length: 16384,
5915 pieces: &pieces,
5916 private: 1,
5917 },
5918 };
5919
5920 let bytes = irontide_bencode::to_bytes(&t).unwrap();
5921 let meta = torrent_from_bytes(&bytes).unwrap();
5922 assert_eq!(meta.info.private, Some(1));
5923
5924 let storage = make_storage(&data, 16384);
5925 let config = test_config();
5926
5927 let (atx, amask) = test_alert_channel();
5928 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
5929 let handle = TorrentHandle::from_torrent(
5930 meta,
5931 irontide_core::TorrentVersion::V1Only,
5932 None,
5933 dh,
5934 dm,
5935 config,
5936 None,
5937 None,
5938 None,
5939 None,
5940 crate::slot_tuner::SlotTuner::disabled(4),
5941 atx,
5942 amask,
5943 None,
5944 None,
5945 test_ban_manager(),
5946 test_ip_filter(),
5947 Arc::new(Vec::new()),
5948 None,
5949 None,
5950 Arc::new(crate::transport::NetworkFactory::tokio()),
5951 None, )
5953 .await
5954 .unwrap();
5955
5956 let stats = handle.stats().await.unwrap();
5957 assert_eq!(stats.state, TorrentState::Downloading);
5958
5959 handle.shutdown().await.unwrap();
5960 }
5961
5962 #[tokio::test]
5965 async fn magnet_no_tracker_before_metadata() {
5966 let magnet = Magnet {
5967 info_hashes: irontide_core::InfoHashes::v1_only(
5968 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
5969 ),
5970 display_name: Some("magnet test".into()),
5971 trackers: vec![],
5972 peers: vec![],
5973 selected_files: None,
5974 };
5975
5976 let (atx, amask) = test_alert_channel();
5977 let (dm, _dj) = test_disk_manager();
5978 let handle = TorrentHandle::from_magnet(
5979 magnet,
5980 dm,
5981 test_config(),
5982 None,
5983 None,
5984 None,
5985 None,
5986 crate::slot_tuner::SlotTuner::disabled(4),
5987 atx,
5988 amask,
5989 None,
5990 None,
5991 test_ban_manager(),
5992 test_ip_filter(),
5993 Arc::new(Vec::new()),
5994 None,
5995 None,
5996 Arc::new(crate::transport::NetworkFactory::tokio()),
5997 None, )
5999 .await
6000 .unwrap();
6001
6002 let stats = handle.stats().await.unwrap();
6003 assert_eq!(stats.state, TorrentState::FetchingMetadata);
6004
6005 tokio::time::sleep(Duration::from_millis(50)).await;
6009
6010 handle.shutdown().await.unwrap();
6011 }
6012
6013 #[tokio::test]
6016 async fn pause_and_resume() {
6017 let data = vec![0xEEu8; 32768];
6018 let meta = make_test_torrent(&data, 16384);
6019 let storage = make_storage(&data, 16384);
6020 let config = test_config();
6021 let (atx, amask) = test_alert_channel();
6022 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6023 let handle = TorrentHandle::from_torrent(
6024 meta,
6025 irontide_core::TorrentVersion::V1Only,
6026 None,
6027 dh,
6028 dm,
6029 config,
6030 None,
6031 None,
6032 None,
6033 None,
6034 crate::slot_tuner::SlotTuner::disabled(4),
6035 atx,
6036 amask,
6037 None,
6038 None,
6039 test_ban_manager(),
6040 test_ip_filter(),
6041 Arc::new(Vec::new()),
6042 None,
6043 None,
6044 Arc::new(crate::transport::NetworkFactory::tokio()),
6045 None, )
6047 .await
6048 .unwrap();
6049
6050 let stats = handle.stats().await.unwrap();
6051 assert_eq!(stats.state, TorrentState::Downloading);
6052
6053 handle.pause().await.unwrap();
6054 tokio::time::sleep(Duration::from_millis(50)).await;
6055 let stats = handle.stats().await.unwrap();
6056 assert_eq!(stats.state, TorrentState::Paused);
6057
6058 handle.resume().await.unwrap();
6059 tokio::time::sleep(Duration::from_millis(50)).await;
6060 let stats = handle.stats().await.unwrap();
6061 assert_eq!(stats.state, TorrentState::Downloading);
6062
6063 handle.shutdown().await.unwrap();
6064 }
6065
6066 #[tokio::test]
6069 async fn pause_already_paused_is_noop() {
6070 let data = vec![0xEEu8; 32768];
6071 let meta = make_test_torrent(&data, 16384);
6072 let storage = make_storage(&data, 16384);
6073 let config = test_config();
6074 let (atx, amask) = test_alert_channel();
6075 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6076 let handle = TorrentHandle::from_torrent(
6077 meta,
6078 irontide_core::TorrentVersion::V1Only,
6079 None,
6080 dh,
6081 dm,
6082 config,
6083 None,
6084 None,
6085 None,
6086 None,
6087 crate::slot_tuner::SlotTuner::disabled(4),
6088 atx,
6089 amask,
6090 None,
6091 None,
6092 test_ban_manager(),
6093 test_ip_filter(),
6094 Arc::new(Vec::new()),
6095 None,
6096 None,
6097 Arc::new(crate::transport::NetworkFactory::tokio()),
6098 None, )
6100 .await
6101 .unwrap();
6102
6103 handle.pause().await.unwrap();
6104 tokio::time::sleep(Duration::from_millis(50)).await;
6105 handle.pause().await.unwrap(); tokio::time::sleep(Duration::from_millis(50)).await;
6107 let stats = handle.stats().await.unwrap();
6108 assert_eq!(stats.state, TorrentState::Paused);
6109
6110 handle.shutdown().await.unwrap();
6111 }
6112
6113 #[tokio::test]
6119 async fn incoming_request_served_from_storage() {
6120 let data = vec![0xABu8; 16384];
6121 let meta = make_test_torrent(&data, 16384);
6122 let info_hash = meta.info_hash;
6123 let storage = make_storage(&data, 16384);
6124
6125 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6126 let listen_addr = listener.local_addr().unwrap();
6127 drop(listener);
6128
6129 let config = TorrentConfig {
6130 listen_port: listen_addr.port(),
6131 ..test_config()
6132 };
6133
6134 let (atx, amask) = test_alert_channel();
6135 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6136 let handle = TorrentHandle::from_torrent(
6137 meta,
6138 irontide_core::TorrentVersion::V1Only,
6139 None,
6140 dh,
6141 dm,
6142 config,
6143 None,
6144 None,
6145 None,
6146 None,
6147 crate::slot_tuner::SlotTuner::disabled(4),
6148 atx,
6149 amask,
6150 None,
6151 None,
6152 test_ban_manager(),
6153 test_ip_filter(),
6154 Arc::new(Vec::new()),
6155 None,
6156 None,
6157 Arc::new(crate::transport::NetworkFactory::tokio()),
6158 None, )
6160 .await
6161 .unwrap();
6162
6163 tokio::time::sleep(Duration::from_millis(50)).await;
6164
6165 let seed_data = data.clone();
6167 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6168 let seeder_task = tokio::spawn({
6169 let info_hash = info_hash;
6170 async move {
6171 let (reader, writer) = tokio::io::split(seed_stream);
6172 let mut writer = writer;
6173 let mut reader = reader;
6174
6175 let hs = Handshake::new(
6176 info_hash,
6177 Id20::from_hex("6666666666666666666666666666666666666666").unwrap(),
6178 );
6179 writer.write_all(&hs.to_bytes()).await.unwrap();
6180 writer.flush().await.unwrap();
6181 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6182 reader.read_exact(&mut hs_buf).await.unwrap();
6183
6184 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6185 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6186
6187 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
6189 let payload = ext_hs.to_bytes().unwrap();
6190 framed_write
6191 .send(Message::Extended { ext_id: 0, payload })
6192 .await
6193 .unwrap();
6194
6195 let mut bf = Bitfield::new(1);
6197 bf.set(0);
6198 framed_write
6199 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6200 .await
6201 .unwrap();
6202 framed_write.send(Message::Unchoke).await.unwrap();
6203
6204 while let Some(Ok(msg)) = framed_read.next().await {
6206 match msg {
6207 Message::Request {
6208 index,
6209 begin,
6210 length,
6211 } => {
6212 let start = begin as usize;
6213 let end = start + length as usize;
6214 framed_write
6215 .send(Message::Piece {
6216 index,
6217 begin,
6218 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
6219 data_1: Bytes::new(),
6220 })
6221 .await
6222 .unwrap();
6223 }
6224 Message::Interested => {}
6225 _ => {}
6226 }
6227 }
6228 }
6229 });
6230
6231 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6233 loop {
6234 tokio::time::sleep(Duration::from_millis(100)).await;
6235 let stats = handle.stats().await.unwrap();
6236 if stats.pieces_have == 1 {
6237 break;
6238 }
6239 if tokio::time::Instant::now() > deadline {
6240 panic!("piece download did not complete within 5s");
6241 }
6242 }
6243
6244 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6246 let expected_data = data.clone();
6247 let leecher_task = tokio::spawn({
6248 let info_hash = info_hash;
6249 async move {
6250 let (reader, writer) = tokio::io::split(leech_stream);
6251 let mut writer = writer;
6252 let mut reader = reader;
6253
6254 let hs = Handshake::new(
6255 info_hash,
6256 Id20::from_hex("7777777777777777777777777777777777777777").unwrap(),
6257 );
6258 writer.write_all(&hs.to_bytes()).await.unwrap();
6259 writer.flush().await.unwrap();
6260 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6261 reader.read_exact(&mut hs_buf).await.unwrap();
6262
6263 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6264 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6265
6266 let _msg = framed_read.next().await; let ext_hs = ExtHandshake::new();
6268 let payload = ext_hs.to_bytes().unwrap();
6269 framed_write
6270 .send(Message::Extended { ext_id: 0, payload })
6271 .await
6272 .unwrap();
6273
6274 framed_write.send(Message::Interested).await.unwrap();
6276
6277 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
6278 loop {
6279 tokio::select! {
6280 msg = framed_read.next() => {
6281 match msg {
6282 Some(Ok(Message::Unchoke)) => { break; }
6283 Some(Ok(_)) => {}
6284 _ => panic!("connection closed before unchoke"),
6285 }
6286 }
6287 _ = tokio::time::sleep_until(deadline) => {
6288 panic!("timed out waiting for unchoke");
6289 }
6290 }
6291 }
6292
6293 framed_write
6295 .send(Message::Request {
6296 index: 0,
6297 begin: 0,
6298 length: 16384,
6299 })
6300 .await
6301 .unwrap();
6302
6303 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6305 loop {
6306 tokio::select! {
6307 msg = framed_read.next() => {
6308 match msg {
6309 Some(Ok(Message::Piece { index, begin, data_0, data_1 })) => {
6310 assert_eq!(index, 0);
6311 assert_eq!(begin, 0);
6312 let _ = &data_1; assert_eq!(data_0.as_ref(), expected_data.as_slice());
6314 return; }
6316 Some(Ok(_)) => {}
6317 Some(Err(e)) => panic!("error reading: {e}"),
6318 None => panic!("connection closed before piece"),
6319 }
6320 }
6321 _ = tokio::time::sleep_until(deadline) => {
6322 panic!("timed out waiting for piece data");
6323 }
6324 }
6325 }
6326 }
6327 });
6328
6329 let result = tokio::time::timeout(Duration::from_secs(20), leecher_task).await;
6331 match result {
6332 Ok(Ok(())) => {}
6333 Ok(Err(e)) => panic!("leecher task panicked: {e}"),
6334 Err(_) => panic!("test timed out"),
6335 }
6336
6337 let stats = handle.stats().await.unwrap();
6339 assert!(
6340 stats.uploaded > 0,
6341 "expected uploaded > 0, got {}",
6342 stats.uploaded
6343 );
6344
6345 handle.shutdown().await.unwrap();
6346 seeder_task.abort();
6347 }
6348
6349 #[tokio::test]
6352 async fn seed_ratio_limit_stops_torrent() {
6353 let data = vec![0xCCu8; 16384];
6356 let meta = make_test_torrent(&data, 16384);
6357 let info_hash = meta.info_hash;
6358 let storage = make_storage(&data, 16384);
6359
6360 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6361 let listen_addr = listener.local_addr().unwrap();
6362 drop(listener);
6363
6364 let config = TorrentConfig {
6365 listen_port: listen_addr.port(),
6366 seed_ratio_limit: Some(1.0),
6367 ..test_config()
6368 };
6369
6370 let (atx, amask) = test_alert_channel();
6371 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6372 let handle = TorrentHandle::from_torrent(
6373 meta,
6374 irontide_core::TorrentVersion::V1Only,
6375 None,
6376 dh,
6377 dm,
6378 config,
6379 None,
6380 None,
6381 None,
6382 None,
6383 crate::slot_tuner::SlotTuner::disabled(4),
6384 atx,
6385 amask,
6386 None,
6387 None,
6388 test_ban_manager(),
6389 test_ip_filter(),
6390 Arc::new(Vec::new()),
6391 None,
6392 None,
6393 Arc::new(crate::transport::NetworkFactory::tokio()),
6394 None, )
6396 .await
6397 .unwrap();
6398
6399 tokio::time::sleep(Duration::from_millis(50)).await;
6400
6401 let seed_data = data.clone();
6403 let seed_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6404 let seeder_task = tokio::spawn({
6405 let info_hash = info_hash;
6406 async move {
6407 let (reader, writer) = tokio::io::split(seed_stream);
6408 let mut writer = writer;
6409 let mut reader = reader;
6410
6411 let hs = Handshake::new(
6412 info_hash,
6413 Id20::from_hex("8888888888888888888888888888888888888888").unwrap(),
6414 );
6415 writer.write_all(&hs.to_bytes()).await.unwrap();
6416 writer.flush().await.unwrap();
6417 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6418 reader.read_exact(&mut hs_buf).await.unwrap();
6419
6420 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6421 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6422
6423 let _msg = framed_read.next().await;
6424 let ext_hs = ExtHandshake::new();
6425 let payload = ext_hs.to_bytes().unwrap();
6426 framed_write
6427 .send(Message::Extended { ext_id: 0, payload })
6428 .await
6429 .unwrap();
6430
6431 let mut bf = Bitfield::new(1);
6432 bf.set(0);
6433 framed_write
6434 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
6435 .await
6436 .unwrap();
6437 framed_write.send(Message::Unchoke).await.unwrap();
6438
6439 while let Some(Ok(msg)) = framed_read.next().await {
6440 match msg {
6441 Message::Request {
6442 index,
6443 begin,
6444 length,
6445 } => {
6446 let start = begin as usize;
6447 let end = start + length as usize;
6448 framed_write
6449 .send(Message::Piece {
6450 index,
6451 begin,
6452 data_0: Bytes::copy_from_slice(&seed_data[start..end]),
6453 data_1: Bytes::new(),
6454 })
6455 .await
6456 .unwrap();
6457 }
6458 Message::Interested => {}
6459 _ => {}
6460 }
6461 }
6462 }
6463 });
6464
6465 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
6467 loop {
6468 tokio::time::sleep(Duration::from_millis(100)).await;
6469 let stats = handle.stats().await.unwrap();
6470 if stats.state == TorrentState::Seeding {
6471 break;
6472 }
6473 if tokio::time::Instant::now() > deadline {
6474 panic!("download did not complete within 5s");
6475 }
6476 }
6477
6478 let leech_stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6480 let leecher_task = tokio::spawn({
6481 let info_hash = info_hash;
6482 async move {
6483 let (reader, writer) = tokio::io::split(leech_stream);
6484 let mut writer = writer;
6485 let mut reader = reader;
6486
6487 let hs = Handshake::new(
6488 info_hash,
6489 Id20::from_hex("9999999999999999999999999999999999999999").unwrap(),
6490 );
6491 writer.write_all(&hs.to_bytes()).await.unwrap();
6492 writer.flush().await.unwrap();
6493 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6494 reader.read_exact(&mut hs_buf).await.unwrap();
6495
6496 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6497 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6498
6499 let _msg = framed_read.next().await;
6500 let ext_hs = ExtHandshake::new();
6501 let payload = ext_hs.to_bytes().unwrap();
6502 framed_write
6503 .send(Message::Extended { ext_id: 0, payload })
6504 .await
6505 .unwrap();
6506
6507 framed_write.send(Message::Interested).await.unwrap();
6508
6509 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
6511 loop {
6512 tokio::select! {
6513 msg = framed_read.next() => {
6514 match msg {
6515 Some(Ok(Message::Unchoke)) => break,
6516 Some(Ok(_)) => {}
6517 _ => return, }
6519 }
6520 _ = tokio::time::sleep_until(deadline) => return,
6521 }
6522 }
6523
6524 framed_write
6526 .send(Message::Request {
6527 index: 0,
6528 begin: 0,
6529 length: 16384,
6530 })
6531 .await
6532 .unwrap();
6533
6534 while let Some(Ok(_msg)) = framed_read.next().await {}
6536 }
6537 });
6538
6539 let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
6541 loop {
6542 tokio::time::sleep(Duration::from_millis(100)).await;
6543 let stats = handle.stats().await.unwrap();
6544 if stats.state == TorrentState::Stopped {
6545 assert!(
6546 stats.uploaded >= 16384,
6547 "expected uploaded >= 16384, got {}",
6548 stats.uploaded
6549 );
6550 break;
6551 }
6552 if tokio::time::Instant::now() > deadline {
6553 let stats = handle.stats().await.unwrap();
6554 panic!(
6555 "expected Stopped, got {:?}, uploaded={}, downloaded={}",
6556 stats.state, stats.uploaded, stats.downloaded
6557 );
6558 }
6559 }
6560
6561 handle.shutdown().await.unwrap();
6562 seeder_task.abort();
6563 leecher_task.abort();
6564 }
6565
6566 #[tokio::test]
6569 async fn resume_with_seeded_storage() {
6570 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
6572 let storage = make_seeded_storage(&data, 16384);
6573 let config = test_config();
6574
6575 let (atx, amask) = test_alert_channel();
6576 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6577 let handle = TorrentHandle::from_torrent(
6578 meta,
6579 irontide_core::TorrentVersion::V1Only,
6580 None,
6581 dh,
6582 dm,
6583 config,
6584 None,
6585 None,
6586 None,
6587 None,
6588 crate::slot_tuner::SlotTuner::disabled(4),
6589 atx,
6590 amask,
6591 None,
6592 None,
6593 test_ban_manager(),
6594 test_ip_filter(),
6595 Arc::new(Vec::new()),
6596 None,
6597 None,
6598 Arc::new(crate::transport::NetworkFactory::tokio()),
6599 None, )
6601 .await
6602 .unwrap();
6603
6604 tokio::time::sleep(Duration::from_millis(100)).await;
6606
6607 let stats = handle.stats().await.unwrap();
6608 assert_eq!(
6609 stats.state,
6610 TorrentState::Seeding,
6611 "should start as seeder with all pieces verified"
6612 );
6613 assert_eq!(stats.pieces_have, 2);
6614 assert_eq!(stats.pieces_total, 2);
6615
6616 handle.shutdown().await.unwrap();
6617 }
6618
6619 #[tokio::test]
6622 async fn save_resume_data_captures_state() {
6623 let data = vec![0xAB; 32768];
6624 let meta = make_test_torrent(&data, 16384);
6625 let info_hash = meta.info_hash;
6626 let storage = make_storage(&data, 16384);
6627 let config = test_config();
6628
6629 let (atx, amask) = test_alert_channel();
6630 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6631 let handle = TorrentHandle::from_torrent(
6632 meta,
6633 irontide_core::TorrentVersion::V1Only,
6634 None,
6635 dh,
6636 dm,
6637 config,
6638 None,
6639 None,
6640 None,
6641 None,
6642 crate::slot_tuner::SlotTuner::disabled(4),
6643 atx,
6644 amask,
6645 None,
6646 None,
6647 test_ban_manager(),
6648 test_ip_filter(),
6649 Arc::new(Vec::new()),
6650 None,
6651 None,
6652 Arc::new(crate::transport::NetworkFactory::tokio()),
6653 None, )
6655 .await
6656 .unwrap();
6657
6658 tokio::time::sleep(Duration::from_millis(50)).await;
6660
6661 let rd = handle.save_resume_data().await.unwrap();
6662
6663 assert_eq!(rd.file_format, "libtorrent resume file");
6664 assert_eq!(rd.file_version, 1);
6665 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
6666 assert_eq!(rd.name, "test");
6667 assert_eq!(rd.save_path, "/tmp");
6668 assert_eq!(rd.paused, 0);
6669 assert!(!rd.pieces.is_empty());
6671 assert_eq!(rd.total_uploaded, 0);
6673 assert_eq!(rd.total_downloaded, 0);
6674
6675 handle.shutdown().await.unwrap();
6676 }
6677
6678 #[tokio::test]
6681 async fn save_resume_data_seeder() {
6682 let data = vec![0xCD; 32768];
6683 let meta = make_test_torrent(&data, 16384);
6684 let info_hash = meta.info_hash;
6685 let storage = make_seeded_storage(&data, 16384);
6686 let config = test_config();
6687
6688 let (atx, amask) = test_alert_channel();
6689 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6690 let handle = TorrentHandle::from_torrent(
6691 meta,
6692 irontide_core::TorrentVersion::V1Only,
6693 None,
6694 dh,
6695 dm,
6696 config,
6697 None,
6698 None,
6699 None,
6700 None,
6701 crate::slot_tuner::SlotTuner::disabled(4),
6702 atx,
6703 amask,
6704 None,
6705 None,
6706 test_ban_manager(),
6707 test_ip_filter(),
6708 Arc::new(Vec::new()),
6709 None,
6710 None,
6711 Arc::new(crate::transport::NetworkFactory::tokio()),
6712 None, )
6714 .await
6715 .unwrap();
6716
6717 tokio::time::sleep(Duration::from_millis(100)).await;
6719
6720 let rd = handle.save_resume_data().await.unwrap();
6721
6722 assert_eq!(rd.info_hash, info_hash.as_bytes().to_vec());
6723 assert_eq!(rd.name, "test");
6724 assert_eq!(rd.seed_mode, 1, "seeder should have seed_mode=1");
6725 assert_eq!(rd.paused, 0);
6726 assert_eq!(rd.pieces.len(), 1);
6729 assert_eq!(
6730 rd.pieces[0] & 0xC0,
6731 0xC0,
6732 "both pieces should be marked complete"
6733 );
6734
6735 handle.shutdown().await.unwrap();
6736 }
6737
6738 #[tokio::test]
6741 async fn save_resume_data_paused() {
6742 let data = vec![0xEF; 16384];
6743 let meta = make_test_torrent(&data, 16384);
6744 let storage = make_storage(&data, 16384);
6745 let config = test_config();
6746
6747 let (atx, amask) = test_alert_channel();
6748 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6749 let handle = TorrentHandle::from_torrent(
6750 meta,
6751 irontide_core::TorrentVersion::V1Only,
6752 None,
6753 dh,
6754 dm,
6755 config,
6756 None,
6757 None,
6758 None,
6759 None,
6760 crate::slot_tuner::SlotTuner::disabled(4),
6761 atx,
6762 amask,
6763 None,
6764 None,
6765 test_ban_manager(),
6766 test_ip_filter(),
6767 Arc::new(Vec::new()),
6768 None,
6769 None,
6770 Arc::new(crate::transport::NetworkFactory::tokio()),
6771 None, )
6773 .await
6774 .unwrap();
6775
6776 tokio::time::sleep(Duration::from_millis(50)).await;
6777 handle.pause().await.unwrap();
6778 tokio::time::sleep(Duration::from_millis(50)).await;
6779
6780 let rd = handle.save_resume_data().await.unwrap();
6781 assert_eq!(rd.paused, 1, "paused torrent should have paused=1");
6782 assert_eq!(rd.seed_mode, 0);
6783
6784 handle.shutdown().await.unwrap();
6785 }
6786
6787 #[tokio::test]
6790 async fn set_file_priority_and_read_back() {
6791 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
6792 let mut torrent_bytes = b"d4:info".to_vec();
6793 torrent_bytes.extend_from_slice(info_bytes);
6794 torrent_bytes.push(b'e');
6795
6796 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
6797 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
6798 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
6799 let config = TorrentConfig {
6800 listen_port: 0,
6801 ..Default::default()
6802 };
6803
6804 let (atx, amask) = test_alert_channel();
6805 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6806 let handle = TorrentHandle::from_torrent(
6807 meta,
6808 irontide_core::TorrentVersion::V1Only,
6809 None,
6810 dh,
6811 dm,
6812 config,
6813 None,
6814 None,
6815 None,
6816 None,
6817 crate::slot_tuner::SlotTuner::disabled(4),
6818 atx,
6819 amask,
6820 None,
6821 None,
6822 test_ban_manager(),
6823 test_ip_filter(),
6824 Arc::new(Vec::new()),
6825 None,
6826 None,
6827 Arc::new(crate::transport::NetworkFactory::tokio()),
6828 None, )
6830 .await
6831 .unwrap();
6832
6833 let prios = handle.file_priorities().await.unwrap();
6835 assert_eq!(prios.len(), 2);
6836 assert!(prios.iter().all(|p| *p == FilePriority::Normal));
6837
6838 handle
6840 .set_file_priority(0, FilePriority::Skip)
6841 .await
6842 .unwrap();
6843
6844 let prios = handle.file_priorities().await.unwrap();
6845 assert_eq!(prios[0], FilePriority::Skip);
6846 assert_eq!(prios[1], FilePriority::Normal);
6847
6848 let result = handle.set_file_priority(99, FilePriority::High).await;
6850 assert!(result.is_err());
6851
6852 handle.shutdown().await.unwrap();
6853 tokio::time::sleep(Duration::from_millis(50)).await;
6854 }
6855
6856 #[tokio::test]
6857 async fn resume_data_preserves_file_priorities() {
6858 let info_bytes = b"d5:filesld6:lengthi100e4:pathl5:a.bineed6:lengthi100e4:pathl5:b.bineee4:name4:test12:piece lengthi100e6:pieces40:AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBe";
6859 let mut torrent_bytes = b"d4:info".to_vec();
6860 torrent_bytes.extend_from_slice(info_bytes);
6861 torrent_bytes.push(b'e');
6862
6863 let meta = irontide_core::torrent_from_bytes(&torrent_bytes).unwrap();
6864 let lengths = Lengths::new(200, 100, DEFAULT_CHUNK_SIZE);
6865 let storage: Arc<dyn TorrentStorage> = Arc::new(MemoryStorage::new(lengths));
6866 let config = TorrentConfig {
6867 listen_port: 0,
6868 ..Default::default()
6869 };
6870
6871 let (atx, amask) = test_alert_channel();
6872 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6873 let handle = TorrentHandle::from_torrent(
6874 meta,
6875 irontide_core::TorrentVersion::V1Only,
6876 None,
6877 dh,
6878 dm,
6879 config,
6880 None,
6881 None,
6882 None,
6883 None,
6884 crate::slot_tuner::SlotTuner::disabled(4),
6885 atx,
6886 amask,
6887 None,
6888 None,
6889 test_ban_manager(),
6890 test_ip_filter(),
6891 Arc::new(Vec::new()),
6892 None,
6893 None,
6894 Arc::new(crate::transport::NetworkFactory::tokio()),
6895 None, )
6897 .await
6898 .unwrap();
6899
6900 handle
6902 .set_file_priority(0, FilePriority::High)
6903 .await
6904 .unwrap();
6905 handle
6906 .set_file_priority(1, FilePriority::Skip)
6907 .await
6908 .unwrap();
6909
6910 let rd = handle.save_resume_data().await.unwrap();
6912 assert_eq!(rd.file_priority, vec![7, 0]); let encoded = irontide_bencode::to_bytes(&rd).unwrap();
6916 let decoded: irontide_core::FastResumeData =
6917 irontide_bencode::from_bytes(&encoded).unwrap();
6918 assert_eq!(decoded.file_priority, vec![7, 0]);
6919
6920 handle.shutdown().await.unwrap();
6921 tokio::time::sleep(Duration::from_millis(50)).await;
6922 }
6923
6924 #[tokio::test]
6927 async fn upload_rate_limiting_caps_throughput() {
6928 let data = vec![0xAB; 16384]; let meta = make_test_torrent(&data, 16384);
6934 let info_hash = meta.info_hash;
6935 let storage = make_seeded_storage(&data, 16384);
6936
6937 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
6938 let listen_addr = listener.local_addr().unwrap();
6939
6940 let config = TorrentConfig {
6941 listen_port: listen_addr.port(),
6942 upload_rate_limit: 1024, ..test_config()
6944 };
6945
6946 drop(listener);
6947 let (atx, amask) = test_alert_channel();
6948 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
6949 let handle = TorrentHandle::from_torrent(
6950 meta,
6951 irontide_core::TorrentVersion::V1Only,
6952 None,
6953 dh,
6954 dm,
6955 config,
6956 None,
6957 None,
6958 None,
6959 None,
6960 crate::slot_tuner::SlotTuner::disabled(4),
6961 atx,
6962 amask,
6963 None,
6964 None,
6965 test_ban_manager(),
6966 test_ip_filter(),
6967 Arc::new(Vec::new()),
6968 None,
6969 None,
6970 Arc::new(crate::transport::NetworkFactory::tokio()),
6971 None, )
6973 .await
6974 .unwrap();
6975
6976 tokio::time::sleep(Duration::from_millis(50)).await;
6977
6978 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
6980 let (reader, writer) = tokio::io::split(stream);
6981 let mut writer = writer;
6982 let mut reader = reader;
6983
6984 let hs = Handshake::new(
6985 info_hash,
6986 Id20::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(),
6987 );
6988 writer.write_all(&hs.to_bytes()).await.unwrap();
6989 writer.flush().await.unwrap();
6990 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
6991 reader.read_exact(&mut hs_buf).await.unwrap();
6992
6993 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
6994 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
6995
6996 let _msg = framed_read.next().await;
6998 let ext_hs = ExtHandshake::new();
6999 let payload = ext_hs.to_bytes().unwrap();
7000 framed_write
7001 .send(Message::Extended { ext_id: 0, payload })
7002 .await
7003 .unwrap();
7004
7005 let _bf_msg = framed_read.next().await;
7007
7008 framed_write.send(Message::Interested).await.unwrap();
7010
7011 let deadline = tokio::time::Instant::now() + Duration::from_secs(15);
7013 loop {
7014 tokio::select! {
7015 msg = framed_read.next() => {
7016 match msg {
7017 Some(Ok(Message::Unchoke)) => break,
7018 Some(Ok(_)) => {}
7019 _ => panic!("connection closed before unchoke"),
7020 }
7021 }
7022 _ = tokio::time::sleep_until(deadline) => {
7023 panic!("timed out waiting for unchoke");
7024 }
7025 }
7026 }
7027
7028 framed_write
7030 .send(Message::Request {
7031 index: 0,
7032 begin: 0,
7033 length: 16384,
7034 })
7035 .await
7036 .unwrap();
7037
7038 let mut got_piece = false;
7042 match tokio::time::timeout(Duration::from_secs(2), async {
7043 loop {
7044 match framed_read.next().await {
7045 Some(Ok(Message::Piece { .. })) => return true,
7046 Some(Ok(_)) => continue,
7047 _ => return false,
7048 }
7049 }
7050 })
7051 .await
7052 {
7053 Ok(true) => got_piece = true,
7054 _ => {}
7055 }
7056
7057 assert!(
7059 !got_piece,
7060 "piece should be delayed by rate limiter (1 KB/s for 16 KB chunk)"
7061 );
7062
7063 let stats = handle.stats().await.unwrap();
7065 assert_eq!(stats.uploaded, 0); handle.shutdown().await.unwrap();
7068 }
7069
7070 #[tokio::test]
7071 async fn unlimited_rate_has_no_effect() {
7072 let data = vec![0xAB; 32768];
7074 let meta = make_test_torrent(&data, 16384);
7075 let storage = make_storage(&data, 16384);
7076 let config = test_config();
7077
7078 assert_eq!(config.upload_rate_limit, 0);
7080 assert_eq!(config.download_rate_limit, 0);
7081
7082 let (atx, amask) = test_alert_channel();
7083 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7084 let handle = TorrentHandle::from_torrent(
7085 meta,
7086 irontide_core::TorrentVersion::V1Only,
7087 None,
7088 dh,
7089 dm,
7090 config,
7091 None,
7092 None,
7093 None,
7094 None,
7095 crate::slot_tuner::SlotTuner::disabled(4),
7096 atx,
7097 amask,
7098 None,
7099 None,
7100 test_ban_manager(),
7101 test_ip_filter(),
7102 Arc::new(Vec::new()),
7103 None,
7104 None,
7105 Arc::new(crate::transport::NetworkFactory::tokio()),
7106 None, )
7108 .await
7109 .unwrap();
7110
7111 let stats = handle.stats().await.unwrap();
7112 assert_eq!(stats.state, TorrentState::Downloading);
7113 assert_eq!(stats.pieces_total, 2);
7114
7115 handle.shutdown().await.unwrap();
7116 }
7117
7118 #[tokio::test]
7119 async fn download_rate_limiting_throttles_requests() {
7120 let data = vec![0xAB; 32768];
7123 let meta = make_test_torrent(&data, 16384);
7124 let info_hash = meta.info_hash;
7125 let storage = make_storage(&data, 16384);
7126
7127 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7128 let listen_addr = listener.local_addr().unwrap();
7129
7130 let config = TorrentConfig {
7131 listen_port: listen_addr.port(),
7132 download_rate_limit: 1024, ..test_config()
7134 };
7135
7136 drop(listener);
7137 let (atx, amask) = test_alert_channel();
7138 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7139 let handle = TorrentHandle::from_torrent(
7140 meta,
7141 irontide_core::TorrentVersion::V1Only,
7142 None,
7143 dh,
7144 dm,
7145 config,
7146 None,
7147 None,
7148 None,
7149 None,
7150 crate::slot_tuner::SlotTuner::disabled(4),
7151 atx,
7152 amask,
7153 None,
7154 None,
7155 test_ban_manager(),
7156 test_ip_filter(),
7157 Arc::new(Vec::new()),
7158 None,
7159 None,
7160 Arc::new(crate::transport::NetworkFactory::tokio()),
7161 None, )
7163 .await
7164 .unwrap();
7165
7166 tokio::time::sleep(Duration::from_millis(50)).await;
7167
7168 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
7170 let (reader, writer) = tokio::io::split(stream);
7171 let mut writer = writer;
7172 let mut reader = reader;
7173
7174 let hs = Handshake::new(
7175 info_hash,
7176 Id20::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap(),
7177 );
7178 writer.write_all(&hs.to_bytes()).await.unwrap();
7179 writer.flush().await.unwrap();
7180 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
7181 reader.read_exact(&mut hs_buf).await.unwrap();
7182
7183 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
7184 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
7185
7186 let _msg = framed_read.next().await;
7188 let ext_hs = ExtHandshake::new();
7189 let payload = ext_hs.to_bytes().unwrap();
7190 framed_write
7191 .send(Message::Extended { ext_id: 0, payload })
7192 .await
7193 .unwrap();
7194
7195 let mut bf = Bitfield::new(2);
7197 bf.set(0);
7198 bf.set(1);
7199 framed_write
7200 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
7201 .await
7202 .unwrap();
7203
7204 framed_write.send(Message::Unchoke).await.unwrap();
7206
7207 let mut requests_received = 0u32;
7211 let deadline = tokio::time::Instant::now() + Duration::from_millis(500);
7212 loop {
7213 match tokio::time::timeout(
7214 deadline.saturating_duration_since(tokio::time::Instant::now()),
7215 framed_read.next(),
7216 )
7217 .await
7218 {
7219 Ok(Some(Ok(Message::Request { .. }))) => {
7220 requests_received += 1;
7221 }
7222 Ok(Some(Ok(_))) => continue,
7223 _ => break,
7224 }
7225 }
7226
7227 let stats = handle.stats().await.unwrap();
7228 assert_eq!(stats.state, TorrentState::Downloading);
7229
7230 assert!(
7233 requests_received <= 2,
7234 "with 1 KB/s limit, should get very few requests, got {requests_received}"
7235 );
7236
7237 handle.shutdown().await.unwrap();
7238 }
7239
7240 #[test]
7243 fn piece_contributor_tracking() {
7244 use std::net::IpAddr;
7245 let mut contributors: HashMap<u32, HashSet<IpAddr>> = HashMap::new();
7246 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
7247 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
7248
7249 contributors.entry(0).or_default().insert(ip1);
7250 contributors.entry(0).or_default().insert(ip2);
7251 assert_eq!(contributors[&0].len(), 2);
7252 assert!(contributors[&0].contains(&ip1));
7253 assert!(contributors[&0].contains(&ip2));
7254
7255 contributors.remove(&0);
7257 assert!(!contributors.contains_key(&0));
7258 }
7259
7260 #[test]
7261 fn parole_enter_on_hash_failure() {
7262 use crate::ban::{BanConfig, BanManager, ParoleState};
7263 use std::net::IpAddr;
7264
7265 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
7266 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
7267 let contributors = vec![ip1, ip2];
7268
7269 let parole = ParoleState {
7271 original_contributors: contributors.into_iter().collect(),
7272 parole_peer: None,
7273 };
7274
7275 assert_eq!(parole.original_contributors.len(), 2);
7276 assert!(parole.original_contributors.contains(&ip1));
7277 assert!(parole.original_contributors.contains(&ip2));
7278 assert!(parole.parole_peer.is_none());
7279 }
7280
7281 #[test]
7282 fn parole_success_strikes_originals() {
7283 use crate::ban::{BanConfig, BanManager, ParoleState};
7284 use std::net::IpAddr;
7285
7286 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
7287 let ip2: IpAddr = "10.0.0.2".parse().unwrap();
7288 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
7289
7290 let mut mgr = BanManager::new(BanConfig {
7291 max_failures: 2,
7292 use_parole: true,
7293 });
7294
7295 let parole = ParoleState {
7296 original_contributors: [ip1, ip2].into_iter().collect(),
7297 parole_peer: Some(parole_ip),
7298 };
7299
7300 for ip in &parole.original_contributors {
7302 mgr.record_strike(*ip);
7303 }
7304
7305 assert_eq!(*mgr.strikes_map().get(&ip1).unwrap(), 1);
7306 assert_eq!(*mgr.strikes_map().get(&ip2).unwrap(), 1);
7307 assert!(!mgr.strikes_map().contains_key(&parole_ip));
7309
7310 for ip in &parole.original_contributors {
7312 mgr.record_strike(*ip);
7313 }
7314 assert!(mgr.is_banned(&ip1));
7315 assert!(mgr.is_banned(&ip2));
7316 }
7317
7318 #[test]
7319 fn parole_failure_strikes_parole_peer() {
7320 use crate::ban::{BanConfig, BanManager, ParoleState};
7321 use std::net::IpAddr;
7322
7323 let ip1: IpAddr = "10.0.0.1".parse().unwrap();
7324 let parole_ip: IpAddr = "10.0.0.3".parse().unwrap();
7325
7326 let mut mgr = BanManager::new(BanConfig {
7327 max_failures: 2,
7328 use_parole: true,
7329 });
7330
7331 let parole = ParoleState {
7332 original_contributors: [ip1].into_iter().collect(),
7333 parole_peer: Some(parole_ip),
7334 };
7335
7336 if let Some(pp) = parole.parole_peer {
7338 mgr.record_strike(pp);
7339 }
7340
7341 assert_eq!(*mgr.strikes_map().get(&parole_ip).unwrap(), 1);
7342 assert!(!mgr.strikes_map().contains_key(&ip1));
7343 }
7344
7345 #[tokio::test]
7346 async fn banned_peer_rejected_on_connect() {
7347 let data = vec![0xAB; 32768];
7348 let meta = make_test_torrent(&data, 16384);
7349 let storage = make_storage(&data, 16384);
7350 let config = test_config();
7351 let ban_mgr = test_ban_manager();
7352
7353 let banned_ip: std::net::IpAddr = "192.168.1.100".parse().unwrap();
7355 ban_mgr.write().ban(banned_ip);
7356
7357 let (atx, amask) = test_alert_channel();
7358 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7359 let handle = TorrentHandle::from_torrent(
7360 meta,
7361 irontide_core::TorrentVersion::V1Only,
7362 None,
7363 dh,
7364 dm,
7365 config,
7366 None,
7367 None,
7368 None,
7369 None,
7370 crate::slot_tuner::SlotTuner::disabled(4),
7371 atx,
7372 amask,
7373 None,
7374 None,
7375 Arc::clone(&ban_mgr),
7376 test_ip_filter(),
7377 Arc::new(Vec::new()),
7378 None,
7379 None,
7380 Arc::new(crate::transport::NetworkFactory::tokio()),
7381 None, )
7383 .await
7384 .unwrap();
7385
7386 handle
7388 .add_peers(
7389 vec![
7390 SocketAddr::new(banned_ip, 6881),
7391 "10.0.0.1:6881".parse().unwrap(),
7392 ],
7393 PeerSource::Tracker,
7394 )
7395 .await
7396 .unwrap();
7397
7398 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
7399 let stats = handle.stats().await.unwrap();
7400 assert!(
7403 stats.peers_available + stats.peers_connected <= 1,
7404 "banned peer should not be added: available={}, connected={}",
7405 stats.peers_available,
7406 stats.peers_connected
7407 );
7408
7409 handle.shutdown().await.unwrap();
7410 }
7411
7412 #[test]
7413 fn banned_peer_filtered_from_available() {
7414 use crate::ban::{BanConfig, BanManager};
7415 use std::net::IpAddr;
7416
7417 let banned_ip: IpAddr = "192.168.1.200".parse().unwrap();
7418 let ok_ip: IpAddr = "10.0.0.1".parse().unwrap();
7419
7420 let mgr = BanManager::new(BanConfig::default());
7421 assert!(!mgr.is_banned(&banned_ip));
7423 assert!(!mgr.is_banned(&ok_ip));
7424
7425 let mut mgr = BanManager::new(BanConfig::default());
7426 mgr.ban(banned_ip);
7427
7428 assert!(mgr.is_banned(&banned_ip));
7430 assert!(!mgr.is_banned(&ok_ip));
7431 }
7432
7433 #[test]
7436 fn hashing_threads_config_default() {
7437 let s = crate::settings::Settings::default();
7438 let expected = {
7439 let cores = std::thread::available_parallelism()
7440 .map(|n| n.get())
7441 .unwrap_or(4);
7442 (cores / 4).clamp(2, 8)
7443 };
7444 assert_eq!(s.hashing_threads, expected);
7445 let tc = TorrentConfig::default();
7446 assert_eq!(tc.hashing_threads, expected);
7447 }
7448
7449 #[tokio::test]
7450 async fn checking_state_and_progress_alerts() {
7451 use crate::alert::{AlertCategory, AlertKind};
7452
7453 let data = vec![0xEEu8; 65536]; let meta = make_test_torrent(&data, 16384);
7455 let storage = make_seeded_storage(&data, 16384);
7456 let config = test_config();
7457
7458 let (atx, amask) = test_alert_channel();
7459 let mut rx = atx.subscribe();
7460 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7461 let handle = TorrentHandle::from_torrent(
7462 meta,
7463 irontide_core::TorrentVersion::V1Only,
7464 None,
7465 dh,
7466 dm,
7467 config,
7468 None,
7469 None,
7470 None,
7471 None,
7472 crate::slot_tuner::SlotTuner::disabled(4),
7473 atx,
7474 amask,
7475 None,
7476 None,
7477 test_ban_manager(),
7478 test_ip_filter(),
7479 Arc::new(Vec::new()),
7480 None,
7481 None,
7482 Arc::new(crate::transport::NetworkFactory::tokio()),
7483 None, )
7485 .await
7486 .unwrap();
7487
7488 let mut saw_checking = false;
7490 let mut progress_values: Vec<f32> = Vec::new();
7491 let mut saw_checked = false;
7492 let mut checked_have = 0u32;
7493 let mut checked_total = 0u32;
7494
7495 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
7496 while tokio::time::Instant::now() < deadline {
7497 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
7498 Ok(Ok(alert)) => match alert.kind {
7499 AlertKind::StateChanged {
7500 new_state: TorrentState::Checking,
7501 ..
7502 } => {
7503 saw_checking = true;
7504 }
7505 AlertKind::CheckingProgress { progress, .. } => {
7506 progress_values.push(progress);
7507 }
7508 AlertKind::TorrentChecked {
7509 pieces_have,
7510 pieces_total,
7511 ..
7512 } => {
7513 saw_checked = true;
7514 checked_have = pieces_have;
7515 checked_total = pieces_total;
7516 break;
7517 }
7518 _ => {}
7519 },
7520 _ => break,
7521 }
7522 }
7523
7524 assert!(saw_checking, "should have seen StateChanged → Checking");
7525 assert!(
7526 !progress_values.is_empty(),
7527 "should have seen CheckingProgress alerts"
7528 );
7529 for w in progress_values.windows(2) {
7531 assert!(
7532 w[1] >= w[0],
7533 "progress should be monotonically increasing: {} < {}",
7534 w[0],
7535 w[1]
7536 );
7537 }
7538 assert!(saw_checked, "should have seen TorrentChecked");
7539 assert_eq!(checked_have, 4);
7540 assert_eq!(checked_total, 4);
7541
7542 tokio::time::sleep(Duration::from_millis(50)).await;
7544 let stats = handle.stats().await.unwrap();
7545 assert_eq!(stats.state, TorrentState::Seeding);
7546
7547 handle.shutdown().await.unwrap();
7548 }
7549
7550 #[tokio::test]
7551 async fn checking_progress_in_stats() {
7552 let data = vec![0xAB; 32768];
7554 let meta = make_test_torrent(&data, 16384);
7555 let storage = make_storage(&data, 16384);
7556 let config = test_config();
7557
7558 let (atx, amask) = test_alert_channel();
7559 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7560 let handle = TorrentHandle::from_torrent(
7561 meta,
7562 irontide_core::TorrentVersion::V1Only,
7563 None,
7564 dh,
7565 dm,
7566 config,
7567 None,
7568 None,
7569 None,
7570 None,
7571 crate::slot_tuner::SlotTuner::disabled(4),
7572 atx,
7573 amask,
7574 None,
7575 None,
7576 test_ban_manager(),
7577 test_ip_filter(),
7578 Arc::new(Vec::new()),
7579 None,
7580 None,
7581 Arc::new(crate::transport::NetworkFactory::tokio()),
7582 None, )
7584 .await
7585 .unwrap();
7586
7587 tokio::time::sleep(Duration::from_millis(100)).await;
7589
7590 let stats = handle.stats().await.unwrap();
7591 assert_eq!(stats.state, TorrentState::Downloading);
7592 assert_eq!(
7593 stats.checking_progress, 0.0,
7594 "checking_progress should be 0.0 when not checking"
7595 );
7596
7597 handle.shutdown().await.unwrap();
7598 }
7599
7600 #[tokio::test]
7601 async fn verify_pieces_partial_data() {
7602 use crate::alert::AlertKind;
7603
7604 let data = vec![0xCCu8; 65536]; let meta = make_test_torrent(&data, 16384);
7607
7608 let lengths = Lengths::new(data.len() as u64, 16384, DEFAULT_CHUNK_SIZE);
7610 let storage = Arc::new(MemoryStorage::new(lengths.clone()));
7611 for p in 0..2u32 {
7612 let offset = lengths.piece_offset(p) as usize;
7613 let size = lengths.piece_size(p) as usize;
7614 storage
7615 .write_chunk(p, 0, &data[offset..offset + size])
7616 .unwrap();
7617 }
7618 let config = test_config();
7621 let (atx, amask) = test_alert_channel();
7622 let mut rx = atx.subscribe();
7623 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7624 let handle = TorrentHandle::from_torrent(
7625 meta,
7626 irontide_core::TorrentVersion::V1Only,
7627 None,
7628 dh,
7629 dm,
7630 config,
7631 None,
7632 None,
7633 None,
7634 None,
7635 crate::slot_tuner::SlotTuner::disabled(4),
7636 atx,
7637 amask,
7638 None,
7639 None,
7640 test_ban_manager(),
7641 test_ip_filter(),
7642 Arc::new(Vec::new()),
7643 None,
7644 None,
7645 Arc::new(crate::transport::NetworkFactory::tokio()),
7646 None, )
7648 .await
7649 .unwrap();
7650
7651 let mut checked_have = 0u32;
7653 let mut checked_total = 0u32;
7654 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
7655 while tokio::time::Instant::now() < deadline {
7656 match tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
7657 Ok(Ok(alert)) => {
7658 if let AlertKind::TorrentChecked {
7659 pieces_have,
7660 pieces_total,
7661 ..
7662 } = alert.kind
7663 {
7664 checked_have = pieces_have;
7665 checked_total = pieces_total;
7666 break;
7667 }
7668 }
7669 _ => break,
7670 }
7671 }
7672
7673 assert_eq!(checked_have, 2, "only 2 pieces should be valid");
7674 assert_eq!(checked_total, 4);
7675
7676 tokio::time::sleep(Duration::from_millis(50)).await;
7678 let stats = handle.stats().await.unwrap();
7679 assert_eq!(stats.state, TorrentState::Downloading);
7680 assert_eq!(stats.pieces_have, 2);
7681 assert_eq!(stats.pieces_total, 4);
7682
7683 handle.shutdown().await.unwrap();
7684 }
7685
7686 #[tokio::test]
7689 async fn ip_filter_blocks_peers_in_handle_add_peers() {
7690 let data = vec![0xCD; 32768];
7691 let meta = make_test_torrent(&data, 16384);
7692 let storage = make_storage(&data, 16384);
7693 let config = test_config();
7694
7695 let ip_filter = {
7697 let mut f = crate::ip_filter::IpFilter::new();
7698 f.add_rule(
7699 "203.0.113.0".parse().unwrap(),
7700 "203.0.113.255".parse().unwrap(),
7701 1,
7702 );
7703 Arc::new(parking_lot::RwLock::new(f))
7704 };
7705
7706 let (atx, amask) = test_alert_channel();
7707 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7708 let handle = TorrentHandle::from_torrent(
7709 meta,
7710 irontide_core::TorrentVersion::V1Only,
7711 None,
7712 dh,
7713 dm,
7714 config,
7715 None,
7716 None,
7717 None,
7718 None,
7719 crate::slot_tuner::SlotTuner::disabled(4),
7720 atx,
7721 amask,
7722 None,
7723 None,
7724 test_ban_manager(),
7725 Arc::clone(&ip_filter),
7726 Arc::new(Vec::new()),
7727 None,
7728 None,
7729 Arc::new(crate::transport::NetworkFactory::tokio()),
7730 None, )
7732 .await
7733 .unwrap();
7734
7735 let blocked_addr: SocketAddr = "203.0.113.42:6881".parse().unwrap();
7737 let allowed_addr: SocketAddr = "198.51.100.1:6881".parse().unwrap();
7738 handle
7739 .add_peers(vec![blocked_addr, allowed_addr], PeerSource::Tracker)
7740 .await
7741 .unwrap();
7742
7743 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
7744 let stats = handle.stats().await.unwrap();
7745 assert!(
7747 stats.peers_available + stats.peers_connected <= 1,
7748 "blocked peer should not be added: available={}, connected={}",
7749 stats.peers_available,
7750 stats.peers_connected
7751 );
7752
7753 handle.shutdown().await.unwrap();
7754 }
7755
7756 #[tokio::test]
7757 async fn set_ip_filter_replaces_filter_and_blocks_new_ip() {
7758 let data = vec![0xCD; 32768];
7761 let meta = make_test_torrent(&data, 16384);
7762 let storage = make_storage(&data, 16384);
7763 let config = test_config();
7764
7765 let ip_filter: crate::session::SharedIpFilter =
7767 Arc::new(parking_lot::RwLock::new(crate::ip_filter::IpFilter::new()));
7768
7769 let (atx, amask) = test_alert_channel();
7770 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7771 let handle = TorrentHandle::from_torrent(
7772 meta,
7773 irontide_core::TorrentVersion::V1Only,
7774 None,
7775 dh,
7776 dm,
7777 config,
7778 None,
7779 None,
7780 None,
7781 None,
7782 crate::slot_tuner::SlotTuner::disabled(4),
7783 atx,
7784 amask,
7785 None,
7786 None,
7787 test_ban_manager(),
7788 Arc::clone(&ip_filter),
7789 Arc::new(Vec::new()),
7790 None,
7791 None,
7792 Arc::new(crate::transport::NetworkFactory::tokio()),
7793 None, )
7795 .await
7796 .unwrap();
7797
7798 let _listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
7801 let local_addr = _listener.local_addr().unwrap();
7802 handle
7803 .add_peers(vec![local_addr], PeerSource::Tracker)
7804 .await
7805 .unwrap();
7806 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
7807 let stats = handle.stats().await.unwrap();
7808 assert!(
7809 stats.peers_available + stats.peers_connected >= 1,
7810 "peer should be allowed initially"
7811 );
7812 handle.shutdown().await.unwrap();
7813
7814 {
7816 let mut f = ip_filter.write();
7817 f.add_rule(
7818 "198.51.100.0".parse().unwrap(),
7819 "198.51.100.255".parse().unwrap(),
7820 1,
7821 );
7822 }
7823
7824 assert!(ip_filter.read().is_blocked("198.51.100.1".parse().unwrap()));
7826 assert!(!ip_filter.read().is_blocked("203.0.113.1".parse().unwrap()));
7828 }
7829
7830 #[test]
7831 fn relocate_files_moves_and_cleans_up() {
7832 let tmp = std::env::temp_dir().join(format!("torrent_relocate_{}", std::process::id()));
7833 let src = tmp.join("src");
7834 let dst = tmp.join("dst");
7835
7836 let subdir = src.join("TorrentName").join("subdir");
7840 std::fs::create_dir_all(&subdir).unwrap();
7841 std::fs::write(subdir.join("file1.txt"), b"hello").unwrap();
7842 std::fs::write(src.join("TorrentName").join("file2.txt"), b"world").unwrap();
7843
7844 let file_paths = vec![
7845 std::path::PathBuf::from("TorrentName/subdir/file1.txt"),
7846 std::path::PathBuf::from("TorrentName/file2.txt"),
7847 ];
7848
7849 relocate_files(&src, &dst, &file_paths).unwrap();
7850
7851 assert_eq!(
7853 std::fs::read_to_string(dst.join("TorrentName/subdir/file1.txt")).unwrap(),
7854 "hello"
7855 );
7856 assert_eq!(
7857 std::fs::read_to_string(dst.join("TorrentName/file2.txt")).unwrap(),
7858 "world"
7859 );
7860
7861 assert!(!src.join("TorrentName").join("subdir").exists());
7863 assert!(!src.join("TorrentName").exists());
7864
7865 let _ = std::fs::remove_dir_all(&tmp);
7867 }
7868
7869 #[test]
7870 fn relocate_files_skips_missing() {
7871 let tmp =
7872 std::env::temp_dir().join(format!("torrent_relocate_skip_{}", std::process::id()));
7873 let src = tmp.join("src");
7874 let dst = tmp.join("dst");
7875 std::fs::create_dir_all(&src).unwrap();
7876
7877 let file_paths = vec![std::path::PathBuf::from("nonexistent.txt")];
7879 relocate_files(&src, &dst, &file_paths).unwrap();
7880
7881 assert!(!dst.join("nonexistent.txt").exists());
7882
7883 let _ = std::fs::remove_dir_all(&tmp);
7884 }
7885
7886 #[tokio::test]
7889 async fn force_recheck_transitions_to_checking() {
7890 let data = vec![0xDDu8; 32768]; let meta = make_test_torrent(&data, 16384);
7892 let storage = make_seeded_storage(&data, 16384);
7893 let config = test_config();
7894
7895 let (atx, amask) = test_alert_channel();
7896 let mut arx = atx.subscribe();
7897 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7898 let handle = TorrentHandle::from_torrent(
7899 meta,
7900 irontide_core::TorrentVersion::V1Only,
7901 None,
7902 dh,
7903 dm,
7904 config,
7905 None,
7906 None,
7907 None,
7908 None,
7909 crate::slot_tuner::SlotTuner::disabled(4),
7910 atx,
7911 amask,
7912 None,
7913 None,
7914 test_ban_manager(),
7915 test_ip_filter(),
7916 Arc::new(Vec::new()),
7917 None,
7918 None,
7919 Arc::new(crate::transport::NetworkFactory::tokio()),
7920 None, )
7922 .await
7923 .unwrap();
7924
7925 tokio::time::sleep(Duration::from_millis(100)).await;
7927 let stats = handle.stats().await.unwrap();
7928 assert_eq!(stats.state, TorrentState::Seeding, "should start as seeder");
7929
7930 while arx.try_recv().is_ok() {}
7932
7933 handle.force_recheck().await.unwrap();
7935
7936 let mut saw_checking = false;
7939 while let Ok(alert) = arx.try_recv() {
7940 if let crate::alert::AlertKind::StateChanged { new_state, .. } = alert.kind {
7941 if new_state == TorrentState::Checking {
7942 saw_checking = true;
7943 }
7944 }
7945 }
7946 assert!(
7947 saw_checking,
7948 "should have transitioned through Checking state"
7949 );
7950
7951 handle.shutdown().await.unwrap();
7952 }
7953
7954 #[tokio::test]
7957 async fn force_recheck_completes() {
7958 let data = vec![0xEEu8; 32768]; let meta = make_test_torrent(&data, 16384);
7960 let storage = make_seeded_storage(&data, 16384);
7961 let config = test_config();
7962
7963 let (atx, amask) = test_alert_channel();
7964 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
7965 let handle = TorrentHandle::from_torrent(
7966 meta,
7967 irontide_core::TorrentVersion::V1Only,
7968 None,
7969 dh,
7970 dm,
7971 config,
7972 None,
7973 None,
7974 None,
7975 None,
7976 crate::slot_tuner::SlotTuner::disabled(4),
7977 atx,
7978 amask,
7979 None,
7980 None,
7981 test_ban_manager(),
7982 test_ip_filter(),
7983 Arc::new(Vec::new()),
7984 None,
7985 None,
7986 Arc::new(crate::transport::NetworkFactory::tokio()),
7987 None, )
7989 .await
7990 .unwrap();
7991
7992 tokio::time::sleep(Duration::from_millis(100)).await;
7994 let stats = handle.stats().await.unwrap();
7995 assert_eq!(stats.state, TorrentState::Seeding);
7996 assert_eq!(stats.pieces_have, 2);
7997
7998 handle.force_recheck().await.unwrap();
8000
8001 let stats = handle.stats().await.unwrap();
8002 assert_eq!(
8003 stats.state,
8004 TorrentState::Seeding,
8005 "should return to Seeding after recheck"
8006 );
8007 assert_eq!(stats.pieces_have, 2, "all pieces should still be verified");
8008
8009 handle.shutdown().await.unwrap();
8010 }
8011
8012 #[tokio::test]
8015 async fn rename_file_succeeds() {
8016 let tmp = std::env::temp_dir().join(format!("torrent_rename_{}", std::process::id()));
8018 std::fs::create_dir_all(&tmp).unwrap();
8019
8020 let data = vec![0xFFu8; 16384]; let meta = make_test_torrent(&data, 16384);
8022 let storage = make_seeded_storage(&data, 16384);
8023
8024 std::fs::write(tmp.join("test"), &data).unwrap();
8027
8028 let mut config = test_config();
8029 config.download_dir = tmp.clone();
8030
8031 let (atx, amask) = test_alert_channel();
8032 let mut arx = atx.subscribe();
8033 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8034 let handle = TorrentHandle::from_torrent(
8035 meta,
8036 irontide_core::TorrentVersion::V1Only,
8037 None,
8038 dh,
8039 dm,
8040 config,
8041 None,
8042 None,
8043 None,
8044 None,
8045 crate::slot_tuner::SlotTuner::disabled(4),
8046 atx,
8047 amask,
8048 None,
8049 None,
8050 test_ban_manager(),
8051 test_ip_filter(),
8052 Arc::new(Vec::new()),
8053 None,
8054 None,
8055 Arc::new(crate::transport::NetworkFactory::tokio()),
8056 None, )
8058 .await
8059 .unwrap();
8060
8061 tokio::time::sleep(Duration::from_millis(100)).await;
8063
8064 while arx.try_recv().is_ok() {}
8066
8067 handle.rename_file(0, "test_renamed".into()).await.unwrap();
8069
8070 assert!(!tmp.join("test").exists(), "old file should be removed");
8072 assert!(tmp.join("test_renamed").exists(), "new file should exist");
8073
8074 let mut saw_renamed = false;
8076 while let Ok(alert) = arx.try_recv() {
8077 if let AlertKind::FileRenamed { index, .. } = alert.kind {
8078 assert_eq!(index, 0);
8079 saw_renamed = true;
8080 }
8081 }
8082 assert!(saw_renamed, "should have received FileRenamed alert");
8083
8084 handle.shutdown().await.unwrap();
8085 let _ = std::fs::remove_dir_all(&tmp);
8086 }
8087
8088 #[tokio::test]
8091 async fn rename_file_invalid_index_errors() {
8092 let data = vec![0xCCu8; 16384]; let meta = make_test_torrent(&data, 16384);
8094 let storage = make_seeded_storage(&data, 16384);
8095 let config = test_config();
8096
8097 let (atx, amask) = test_alert_channel();
8098 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8099 let handle = TorrentHandle::from_torrent(
8100 meta,
8101 irontide_core::TorrentVersion::V1Only,
8102 None,
8103 dh,
8104 dm,
8105 config,
8106 None,
8107 None,
8108 None,
8109 None,
8110 crate::slot_tuner::SlotTuner::disabled(4),
8111 atx,
8112 amask,
8113 None,
8114 None,
8115 test_ban_manager(),
8116 test_ip_filter(),
8117 Arc::new(Vec::new()),
8118 None,
8119 None,
8120 Arc::new(crate::transport::NetworkFactory::tokio()),
8121 None, )
8123 .await
8124 .unwrap();
8125
8126 tokio::time::sleep(Duration::from_millis(100)).await;
8128
8129 let result = handle.rename_file(99, "bad".into()).await;
8131 assert!(result.is_err(), "should fail for out-of-range file index");
8132
8133 handle.shutdown().await.unwrap();
8134 }
8135
8136 #[tokio::test]
8139 async fn file_completed_alert_fires() {
8140 let data = vec![0xBBu8; 32768]; let meta = make_test_torrent(&data, 16384);
8142 let storage = make_seeded_storage(&data, 16384);
8143 let config = test_config();
8144
8145 let (atx, amask) = test_alert_channel();
8146 let mut arx = atx.subscribe();
8147 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8148 let handle = TorrentHandle::from_torrent(
8149 meta,
8150 irontide_core::TorrentVersion::V1Only,
8151 None,
8152 dh,
8153 dm,
8154 config,
8155 None,
8156 None,
8157 None,
8158 None,
8159 crate::slot_tuner::SlotTuner::disabled(4),
8160 atx,
8161 amask,
8162 None,
8163 None,
8164 test_ban_manager(),
8165 test_ip_filter(),
8166 Arc::new(Vec::new()),
8167 None,
8168 None,
8169 Arc::new(crate::transport::NetworkFactory::tokio()),
8170 None, )
8172 .await
8173 .unwrap();
8174
8175 tokio::time::sleep(Duration::from_millis(200)).await;
8177
8178 let mut saw_file_completed = false;
8180 while let Ok(alert) = arx.try_recv() {
8181 if let AlertKind::FileCompleted { file_index, .. } = alert.kind {
8182 assert_eq!(file_index, 0, "should be file index 0");
8183 saw_file_completed = true;
8184 }
8185 }
8186 assert!(
8187 saw_file_completed,
8188 "should have received FileCompleted alert"
8189 );
8190
8191 handle.shutdown().await.unwrap();
8192 }
8193
8194 #[test]
8197 fn metadata_failed_alert_fires() {
8198 let info_hash = Id20::from([0u8; 20]);
8200 let alert = crate::alert::Alert::new(AlertKind::MetadataFailed { info_hash });
8201 assert!(
8202 alert
8203 .category()
8204 .contains(crate::alert::AlertCategory::STATUS),
8205 "MetadataFailed should have STATUS category"
8206 );
8207 assert!(
8208 alert
8209 .category()
8210 .contains(crate::alert::AlertCategory::ERROR),
8211 "MetadataFailed should have ERROR category"
8212 );
8213
8214 let (tx, mut rx) = broadcast::channel(16);
8216 let mask = Arc::new(AtomicU32::new(crate::alert::AlertCategory::ALL.bits()));
8217 post_alert(&tx, &mask, AlertKind::MetadataFailed { info_hash });
8218 let received = rx.try_recv().expect("should receive MetadataFailed alert");
8219 assert!(matches!(received.kind, AlertKind::MetadataFailed { .. }));
8220 }
8221
8222 #[tokio::test]
8225 async fn set_max_connections_persists() {
8226 let data = vec![0xAB; 32768];
8227 let meta = make_test_torrent(&data, 16384);
8228 let storage = make_storage(&data, 16384);
8229 let config = test_config();
8230
8231 let (atx, amask) = test_alert_channel();
8232 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8233 let handle = TorrentHandle::from_torrent(
8234 meta,
8235 irontide_core::TorrentVersion::V1Only,
8236 None,
8237 dh,
8238 dm,
8239 config,
8240 None,
8241 None,
8242 None,
8243 None,
8244 crate::slot_tuner::SlotTuner::disabled(4),
8245 atx,
8246 amask,
8247 None,
8248 None,
8249 test_ban_manager(),
8250 test_ip_filter(),
8251 Arc::new(Vec::new()),
8252 None,
8253 None,
8254 Arc::new(crate::transport::NetworkFactory::tokio()),
8255 None, )
8257 .await
8258 .unwrap();
8259
8260 handle.set_max_connections(10).await.unwrap();
8262 let val = handle.max_connections().await.unwrap();
8263 assert_eq!(val, 10);
8264
8265 handle.set_max_connections(25).await.unwrap();
8267 let val = handle.max_connections().await.unwrap();
8268 assert_eq!(val, 25);
8269
8270 let stats = handle.stats().await.unwrap();
8272 assert_eq!(stats.connections_limit, 25);
8273
8274 handle.shutdown().await.unwrap();
8275 }
8276
8277 #[tokio::test]
8280 async fn max_connections_default() {
8281 let data = vec![0xAB; 32768];
8282 let meta = make_test_torrent(&data, 16384);
8283 let storage = make_storage(&data, 16384);
8284 let config = test_config();
8285 let expected_default = config.max_peers;
8286
8287 let (atx, amask) = test_alert_channel();
8288 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8289 let handle = TorrentHandle::from_torrent(
8290 meta,
8291 irontide_core::TorrentVersion::V1Only,
8292 None,
8293 dh,
8294 dm,
8295 config,
8296 None,
8297 None,
8298 None,
8299 None,
8300 crate::slot_tuner::SlotTuner::disabled(4),
8301 atx,
8302 amask,
8303 None,
8304 None,
8305 test_ban_manager(),
8306 test_ip_filter(),
8307 Arc::new(Vec::new()),
8308 None,
8309 None,
8310 Arc::new(crate::transport::NetworkFactory::tokio()),
8311 None, )
8313 .await
8314 .unwrap();
8315
8316 let val = handle.max_connections().await.unwrap();
8318 assert_eq!(val, 0);
8319
8320 let stats = handle.stats().await.unwrap();
8322 assert_eq!(stats.connections_limit, expected_default);
8323
8324 handle.shutdown().await.unwrap();
8325 }
8326
8327 #[tokio::test]
8330 async fn set_max_uploads_round_trip() {
8331 let data = vec![0xAB; 32768];
8332 let meta = make_test_torrent(&data, 16384);
8333 let storage = make_storage(&data, 16384);
8334 let config = test_config();
8335
8336 let (atx, amask) = test_alert_channel();
8337 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8338 let handle = TorrentHandle::from_torrent(
8339 meta,
8340 irontide_core::TorrentVersion::V1Only,
8341 None,
8342 dh,
8343 dm,
8344 config,
8345 None,
8346 None,
8347 None,
8348 None,
8349 crate::slot_tuner::SlotTuner::disabled(4),
8350 atx,
8351 amask,
8352 None,
8353 None,
8354 test_ban_manager(),
8355 test_ip_filter(),
8356 Arc::new(Vec::new()),
8357 None,
8358 None,
8359 Arc::new(crate::transport::NetworkFactory::tokio()),
8360 None, )
8362 .await
8363 .unwrap();
8364
8365 handle.set_max_uploads(8).await.unwrap();
8367 let val = handle.max_uploads().await.unwrap();
8368 assert_eq!(val, 8);
8369
8370 let stats = handle.stats().await.unwrap();
8372 assert_eq!(stats.uploads_limit, 8);
8373
8374 handle.shutdown().await.unwrap();
8375 }
8376
8377 #[tokio::test]
8380 async fn external_ip_detected_alert() {
8381 let data = vec![0xAB; 32768];
8382 let meta = make_test_torrent(&data, 16384);
8383 let info_hash = meta.info_hash;
8384 let storage = make_storage(&data, 16384);
8385 let config = test_config();
8386
8387 let (atx, amask) = test_alert_channel();
8388 let mut arx = atx.subscribe();
8389 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8390 let handle = TorrentHandle::from_torrent(
8391 meta,
8392 irontide_core::TorrentVersion::V1Only,
8393 None,
8394 dh,
8395 dm,
8396 config,
8397 None,
8398 None,
8399 None,
8400 None,
8401 crate::slot_tuner::SlotTuner::disabled(4),
8402 atx,
8403 amask,
8404 None,
8405 None,
8406 test_ban_manager(),
8407 test_ip_filter(),
8408 Arc::new(Vec::new()),
8409 None,
8410 None,
8411 Arc::new(crate::transport::NetworkFactory::tokio()),
8412 None, )
8414 .await
8415 .unwrap();
8416
8417 while arx.try_recv().is_ok() {}
8419
8420 let test_ip: std::net::IpAddr = "203.0.113.42".parse().unwrap();
8422 handle
8423 .cmd_tx
8424 .send(TorrentCommand::UpdateExternalIp { ip: test_ip })
8425 .await
8426 .unwrap();
8427
8428 tokio::time::sleep(Duration::from_millis(50)).await;
8430
8431 let mut saw_alert = false;
8433 while let Ok(alert) = arx.try_recv() {
8434 if let AlertKind::ExternalIpDetected { ip } = alert.kind {
8435 assert_eq!(ip, test_ip);
8436 saw_alert = true;
8437 }
8438 }
8439 assert!(saw_alert, "should have received ExternalIpDetected alert");
8440
8441 handle.shutdown().await.unwrap();
8442 }
8443
8444 #[tokio::test]
8447 async fn get_peer_info_returns_connected_peers() {
8448 let data = vec![0xAB; 65536]; let meta = make_test_torrent(&data, 16384); let storage = make_storage(&data, 16384);
8451 let config = test_config();
8452
8453 let (atx, amask) = test_alert_channel();
8454 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8455 let handle = TorrentHandle::from_torrent(
8456 meta.clone(),
8457 irontide_core::TorrentVersion::V1Only,
8458 None,
8459 dh,
8460 dm,
8461 config,
8462 None,
8463 None,
8464 None,
8465 None,
8466 crate::slot_tuner::SlotTuner::disabled(4),
8467 atx,
8468 amask,
8469 None,
8470 None,
8471 test_ban_manager(),
8472 test_ip_filter(),
8473 Arc::new(Vec::new()),
8474 None,
8475 None,
8476 Arc::new(crate::transport::NetworkFactory::tokio()),
8477 None, )
8479 .await
8480 .unwrap();
8481
8482 let stats = handle.stats().await.unwrap();
8484 let listen_port = stats.peers_connected; let peer_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
8488 let peer_addr = peer_listener.local_addr().unwrap();
8489
8490 handle
8491 .add_peers(vec![peer_addr], PeerSource::Tracker)
8492 .await
8493 .unwrap();
8494
8495 let accept_timeout =
8497 tokio::time::timeout(Duration::from_secs(2), peer_listener.accept()).await;
8498 if let Ok(Ok((mut stream, _))) = accept_timeout {
8499 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
8501 if tokio::time::timeout(Duration::from_millis(500), stream.read_exact(&mut hs_buf))
8502 .await
8503 .is_ok()
8504 {
8505 let hs = Handshake::new(meta.info_hash, Id20::from([0xBB; 20]));
8507 let hs_bytes = hs.to_bytes();
8508 let _ = stream.write_all(&hs_bytes).await;
8509
8510 tokio::time::sleep(Duration::from_millis(200)).await;
8512
8513 let peer_info = handle.get_peer_info().await.unwrap();
8515 if !peer_info.is_empty() {
8517 let p = &peer_info[0];
8518 assert!(p.peer_choking, "peer should be choking us initially");
8520 assert!(
8522 !p.am_choking,
8523 "we should not be choking peer after connect (M107 unconditional unchoke)"
8524 );
8525 assert!(
8526 !p.peer_interested,
8527 "peer should not be interested initially"
8528 );
8529 assert_eq!(p.num_pieces, 0);
8530 assert_eq!(p.source, PeerSource::Tracker);
8531 }
8532 }
8533 }
8534 let _ = handle.get_peer_info().await.unwrap();
8536 assert_eq!(listen_port, 0); handle.shutdown().await.unwrap();
8539 }
8540
8541 #[tokio::test]
8544 async fn get_peer_info_empty_when_no_peers() {
8545 let data = vec![0xAB; 32768];
8546 let meta = make_test_torrent(&data, 16384);
8547 let storage = make_storage(&data, 16384);
8548 let config = test_config();
8549
8550 let (atx, amask) = test_alert_channel();
8551 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8552 let handle = TorrentHandle::from_torrent(
8553 meta,
8554 irontide_core::TorrentVersion::V1Only,
8555 None,
8556 dh,
8557 dm,
8558 config,
8559 None,
8560 None,
8561 None,
8562 None,
8563 crate::slot_tuner::SlotTuner::disabled(4),
8564 atx,
8565 amask,
8566 None,
8567 None,
8568 test_ban_manager(),
8569 test_ip_filter(),
8570 Arc::new(Vec::new()),
8571 None,
8572 None,
8573 Arc::new(crate::transport::NetworkFactory::tokio()),
8574 None, )
8576 .await
8577 .unwrap();
8578
8579 let peer_info = handle.get_peer_info().await.unwrap();
8580 assert!(peer_info.is_empty(), "should have no peers initially");
8581
8582 handle.shutdown().await.unwrap();
8583 }
8584
8585 #[tokio::test]
8588 async fn get_download_queue_empty_initially() {
8589 let data = vec![0xAB; 32768];
8590 let meta = make_test_torrent(&data, 16384);
8591 let storage = make_storage(&data, 16384);
8592 let config = test_config();
8593
8594 let (atx, amask) = test_alert_channel();
8595 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8596 let handle = TorrentHandle::from_torrent(
8597 meta,
8598 irontide_core::TorrentVersion::V1Only,
8599 None,
8600 dh,
8601 dm,
8602 config,
8603 None,
8604 None,
8605 None,
8606 None,
8607 crate::slot_tuner::SlotTuner::disabled(4),
8608 atx,
8609 amask,
8610 None,
8611 None,
8612 test_ban_manager(),
8613 test_ip_filter(),
8614 Arc::new(Vec::new()),
8615 None,
8616 None,
8617 Arc::new(crate::transport::NetworkFactory::tokio()),
8618 None, )
8620 .await
8621 .unwrap();
8622
8623 let queue = handle.get_download_queue().await.unwrap();
8624 assert!(
8625 queue.is_empty(),
8626 "download queue should be empty with no active downloads"
8627 );
8628
8629 handle.shutdown().await.unwrap();
8630 }
8631
8632 #[tokio::test]
8635 async fn have_piece_false_initially() {
8636 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
8638 let storage = make_storage(&data, 16384);
8639 let config = test_config();
8640
8641 let (atx, amask) = test_alert_channel();
8642 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8643 let handle = TorrentHandle::from_torrent(
8644 meta,
8645 irontide_core::TorrentVersion::V1Only,
8646 None,
8647 dh,
8648 dm,
8649 config,
8650 None,
8651 None,
8652 None,
8653 None,
8654 crate::slot_tuner::SlotTuner::disabled(4),
8655 atx,
8656 amask,
8657 None,
8658 None,
8659 test_ban_manager(),
8660 test_ip_filter(),
8661 Arc::new(Vec::new()),
8662 None,
8663 None,
8664 Arc::new(crate::transport::NetworkFactory::tokio()),
8665 None, )
8667 .await
8668 .unwrap();
8669
8670 assert!(
8671 !handle.have_piece(0).await.unwrap(),
8672 "piece 0 should not be downloaded initially"
8673 );
8674 assert!(
8675 !handle.have_piece(1).await.unwrap(),
8676 "piece 1 should not be downloaded initially"
8677 );
8678
8679 handle.shutdown().await.unwrap();
8680 }
8681
8682 #[tokio::test]
8685 async fn piece_availability_empty_no_peers() {
8686 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
8688 let storage = make_storage(&data, 16384);
8689 let config = test_config();
8690
8691 let (atx, amask) = test_alert_channel();
8692 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8693 let handle = TorrentHandle::from_torrent(
8694 meta,
8695 irontide_core::TorrentVersion::V1Only,
8696 None,
8697 dh,
8698 dm,
8699 config,
8700 None,
8701 None,
8702 None,
8703 None,
8704 crate::slot_tuner::SlotTuner::disabled(4),
8705 atx,
8706 amask,
8707 None,
8708 None,
8709 test_ban_manager(),
8710 test_ip_filter(),
8711 Arc::new(Vec::new()),
8712 None,
8713 None,
8714 Arc::new(crate::transport::NetworkFactory::tokio()),
8715 None, )
8717 .await
8718 .unwrap();
8719
8720 let avail = handle.piece_availability().await.unwrap();
8721 assert_eq!(avail.len(), 2, "should have availability for 2 pieces");
8722 assert!(
8723 avail.iter().all(|&c| c == 0),
8724 "all availability counts should be 0 with no peers"
8725 );
8726
8727 handle.shutdown().await.unwrap();
8728 }
8729
8730 #[tokio::test]
8733 async fn file_progress_zeros_initially() {
8734 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384);
8736 let storage = make_storage(&data, 16384);
8737 let config = test_config();
8738
8739 let (atx, amask) = test_alert_channel();
8740 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8741 let handle = TorrentHandle::from_torrent(
8742 meta,
8743 irontide_core::TorrentVersion::V1Only,
8744 None,
8745 dh,
8746 dm,
8747 config,
8748 None,
8749 None,
8750 None,
8751 None,
8752 crate::slot_tuner::SlotTuner::disabled(4),
8753 atx,
8754 amask,
8755 None,
8756 None,
8757 test_ban_manager(),
8758 test_ip_filter(),
8759 Arc::new(Vec::new()),
8760 None,
8761 None,
8762 Arc::new(crate::transport::NetworkFactory::tokio()),
8763 None, )
8765 .await
8766 .unwrap();
8767
8768 let progress = handle.file_progress().await.unwrap();
8769 assert_eq!(progress.len(), 1, "single-file torrent should have 1 entry");
8770 assert_eq!(progress[0], 0, "no bytes should be downloaded initially");
8771
8772 handle.shutdown().await.unwrap();
8773 }
8774
8775 fn make_test_torrent_multi(
8779 data: &[u8],
8780 piece_length: u64,
8781 file_lengths: &[u64],
8782 ) -> TorrentMetaV1 {
8783 use serde::Serialize;
8784
8785 let mut pieces = Vec::new();
8786 let mut offset = 0;
8787 while offset < data.len() {
8788 let end = (offset + piece_length as usize).min(data.len());
8789 let hash = irontide_core::sha1(&data[offset..end]);
8790 pieces.extend_from_slice(hash.as_bytes());
8791 offset = end;
8792 }
8793
8794 #[derive(Serialize)]
8795 struct FileE {
8796 length: u64,
8797 path: Vec<String>,
8798 }
8799
8800 #[derive(Serialize)]
8801 struct Info<'a> {
8802 name: &'a str,
8803 #[serde(rename = "piece length")]
8804 piece_length: u64,
8805 #[serde(with = "serde_bytes")]
8806 pieces: &'a [u8],
8807 files: Vec<FileE>,
8808 }
8809
8810 #[derive(Serialize)]
8811 struct Torrent<'a> {
8812 info: Info<'a>,
8813 }
8814
8815 let files: Vec<FileE> = file_lengths
8816 .iter()
8817 .enumerate()
8818 .map(|(i, &len)| FileE {
8819 length: len,
8820 path: vec![format!("file{i}.bin")],
8821 })
8822 .collect();
8823
8824 let t = Torrent {
8825 info: Info {
8826 name: "test_multi",
8827 piece_length,
8828 pieces: &pieces,
8829 files,
8830 },
8831 };
8832
8833 let bytes = irontide_bencode::to_bytes(&t).unwrap();
8834 torrent_from_bytes(&bytes).unwrap()
8835 }
8836
8837 #[tokio::test]
8838 async fn file_progress_length_matches_file_count() {
8839 let data = vec![0xCD; 32768];
8841 let file_lengths = [10000u64, 20000, 2768];
8842 let meta = make_test_torrent_multi(&data, 16384, &file_lengths);
8843 let storage = make_storage(&data, 16384);
8844 let config = test_config();
8845
8846 let (atx, amask) = test_alert_channel();
8847 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8848 let handle = TorrentHandle::from_torrent(
8849 meta,
8850 irontide_core::TorrentVersion::V1Only,
8851 None,
8852 dh,
8853 dm,
8854 config,
8855 None,
8856 None,
8857 None,
8858 None,
8859 crate::slot_tuner::SlotTuner::disabled(4),
8860 atx,
8861 amask,
8862 None,
8863 None,
8864 test_ban_manager(),
8865 test_ip_filter(),
8866 Arc::new(Vec::new()),
8867 None,
8868 None,
8869 Arc::new(crate::transport::NetworkFactory::tokio()),
8870 None, )
8872 .await
8873 .unwrap();
8874
8875 let progress = handle.file_progress().await.unwrap();
8876 assert_eq!(
8877 progress.len(),
8878 3,
8879 "multi-file torrent should have 3 entries"
8880 );
8881 assert!(
8882 progress.iter().all(|&b| b == 0),
8883 "all progress should be 0 initially"
8884 );
8885
8886 handle.shutdown().await.unwrap();
8887 }
8888
8889 #[tokio::test]
8892 async fn is_valid_true_for_active() {
8893 let data = vec![0xAB; 32768];
8894 let meta = make_test_torrent(&data, 16384);
8895 let storage = make_storage(&data, 16384);
8896 let config = test_config();
8897
8898 let (atx, amask) = test_alert_channel();
8899 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8900 let handle = TorrentHandle::from_torrent(
8901 meta,
8902 irontide_core::TorrentVersion::V1Only,
8903 None,
8904 dh,
8905 dm,
8906 config,
8907 None,
8908 None,
8909 None,
8910 None,
8911 crate::slot_tuner::SlotTuner::disabled(4),
8912 atx,
8913 amask,
8914 None,
8915 None,
8916 test_ban_manager(),
8917 test_ip_filter(),
8918 Arc::new(Vec::new()),
8919 None,
8920 None,
8921 Arc::new(crate::transport::NetworkFactory::tokio()),
8922 None, )
8924 .await
8925 .unwrap();
8926
8927 assert!(
8928 handle.is_valid(),
8929 "handle should be valid while torrent actor is alive"
8930 );
8931
8932 handle.shutdown().await.unwrap();
8933 }
8934
8935 #[tokio::test]
8938 async fn is_valid_false_after_remove() {
8939 let data = vec![0xAB; 32768];
8940 let meta = make_test_torrent(&data, 16384);
8941 let storage = make_storage(&data, 16384);
8942 let config = test_config();
8943
8944 let (atx, amask) = test_alert_channel();
8945 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8946 let handle = TorrentHandle::from_torrent(
8947 meta,
8948 irontide_core::TorrentVersion::V1Only,
8949 None,
8950 dh,
8951 dm,
8952 config,
8953 None,
8954 None,
8955 None,
8956 None,
8957 crate::slot_tuner::SlotTuner::disabled(4),
8958 atx,
8959 amask,
8960 None,
8961 None,
8962 test_ban_manager(),
8963 test_ip_filter(),
8964 Arc::new(Vec::new()),
8965 None,
8966 None,
8967 Arc::new(crate::transport::NetworkFactory::tokio()),
8968 None, )
8970 .await
8971 .unwrap();
8972
8973 assert!(handle.is_valid());
8974
8975 handle.shutdown().await.unwrap();
8977
8978 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
8980
8981 assert!(
8982 !handle.is_valid(),
8983 "handle should be invalid after shutdown"
8984 );
8985 }
8986
8987 #[tokio::test]
8990 async fn clear_error_resets() {
8991 let data = vec![0xAB; 32768];
8992 let meta = make_test_torrent(&data, 16384);
8993 let storage = make_storage(&data, 16384);
8994 let config = test_config();
8995
8996 let (atx, amask) = test_alert_channel();
8997 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
8998 let handle = TorrentHandle::from_torrent(
8999 meta,
9000 irontide_core::TorrentVersion::V1Only,
9001 None,
9002 dh,
9003 dm,
9004 config,
9005 None,
9006 None,
9007 None,
9008 None,
9009 crate::slot_tuner::SlotTuner::disabled(4),
9010 atx,
9011 amask,
9012 None,
9013 None,
9014 test_ban_manager(),
9015 test_ip_filter(),
9016 Arc::new(Vec::new()),
9017 None,
9018 None,
9019 Arc::new(crate::transport::NetworkFactory::tokio()),
9020 None, )
9022 .await
9023 .unwrap();
9024
9025 let stats = handle.stats().await.unwrap();
9027 assert!(stats.error.is_empty());
9028 assert_eq!(stats.error_file, -1);
9029
9030 handle.clear_error().await.unwrap();
9032
9033 let stats = handle.stats().await.unwrap();
9034 assert!(stats.error.is_empty());
9035 assert_eq!(stats.error_file, -1);
9036
9037 handle.shutdown().await.unwrap();
9038 }
9039
9040 #[tokio::test]
9043 async fn flags_round_trip() {
9044 let data = vec![0xAB; 32768];
9045 let meta = make_test_torrent(&data, 16384);
9046 let storage = make_storage(&data, 16384);
9047 let config = test_config();
9048
9049 let (atx, amask) = test_alert_channel();
9050 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9051 let handle = TorrentHandle::from_torrent(
9052 meta,
9053 irontide_core::TorrentVersion::V1Only,
9054 None,
9055 dh,
9056 dm,
9057 config,
9058 None,
9059 None,
9060 None,
9061 None,
9062 crate::slot_tuner::SlotTuner::disabled(4),
9063 atx,
9064 amask,
9065 None,
9066 None,
9067 test_ban_manager(),
9068 test_ip_filter(),
9069 Arc::new(Vec::new()),
9070 None,
9071 None,
9072 Arc::new(crate::transport::NetworkFactory::tokio()),
9073 None, )
9075 .await
9076 .unwrap();
9077
9078 let initial = handle.flags().await.unwrap();
9080 assert!(!initial.contains(crate::types::TorrentFlags::PAUSED));
9081 assert!(!initial.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
9082 assert!(!initial.contains(crate::types::TorrentFlags::SUPER_SEEDING));
9083
9084 handle
9086 .set_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
9087 .await
9088 .unwrap();
9089 let after_set = handle.flags().await.unwrap();
9090 assert!(after_set.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
9091
9092 handle
9094 .unset_flags(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD)
9095 .await
9096 .unwrap();
9097 let after_unset = handle.flags().await.unwrap();
9098 assert!(!after_unset.contains(crate::types::TorrentFlags::SEQUENTIAL_DOWNLOAD));
9099
9100 assert!(!handle.is_sequential_download().await.unwrap());
9102
9103 handle.shutdown().await.unwrap();
9104 }
9105
9106 #[tokio::test]
9109 async fn connect_peer_no_error() {
9110 let data = vec![0xAB; 32768];
9111 let meta = make_test_torrent(&data, 16384);
9112 let storage = make_storage(&data, 16384);
9113 let config = test_config();
9114
9115 let (atx, amask) = test_alert_channel();
9116 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
9117 let handle = TorrentHandle::from_torrent(
9118 meta,
9119 irontide_core::TorrentVersion::V1Only,
9120 None,
9121 dh,
9122 dm,
9123 config,
9124 None,
9125 None,
9126 None,
9127 None,
9128 crate::slot_tuner::SlotTuner::disabled(4),
9129 atx,
9130 amask,
9131 None,
9132 None,
9133 test_ban_manager(),
9134 test_ip_filter(),
9135 Arc::new(Vec::new()),
9136 None,
9137 None,
9138 Arc::new(crate::transport::NetworkFactory::tokio()),
9139 None, )
9141 .await
9142 .unwrap();
9143
9144 let addr: SocketAddr = "127.0.0.1:12345".parse().unwrap();
9147 handle.connect_peer(addr).await.unwrap();
9148
9149 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
9151
9152 handle.shutdown().await.unwrap();
9153 }
9154
9155 fn make_test_meta_v2(
9159 piece_hashes: &[irontide_core::Id32],
9160 file_root: irontide_core::Id32,
9161 piece_length: u64,
9162 file_length: u64,
9163 ) -> irontide_core::TorrentMetaV2 {
9164 use std::collections::BTreeMap;
9165
9166 let mut layer_bytes = Vec::with_capacity(piece_hashes.len() * 32);
9168 for h in piece_hashes {
9169 layer_bytes.extend_from_slice(&h.0);
9170 }
9171
9172 let mut piece_layers = BTreeMap::new();
9173 piece_layers.insert(file_root, layer_bytes);
9174
9175 let file_tree = irontide_core::FileTreeNode::Directory({
9176 let mut children = BTreeMap::new();
9177 children.insert(
9178 "test.dat".to_string(),
9179 irontide_core::FileTreeNode::File(irontide_core::V2FileAttr {
9180 length: file_length,
9181 pieces_root: Some(file_root),
9182 }),
9183 );
9184 children
9185 });
9186
9187 irontide_core::TorrentMetaV2 {
9188 info_hashes: irontide_core::InfoHashes::v2_only(irontide_core::Id32::ZERO),
9189 info_bytes: None,
9190 announce: None,
9191 announce_list: None,
9192 comment: None,
9193 created_by: None,
9194 creation_date: None,
9195 info: irontide_core::InfoDictV2 {
9196 name: "test".to_string(),
9197 piece_length,
9198 meta_version: 2,
9199 file_tree,
9200 ssl_cert: None,
9201 },
9202 piece_layers,
9203 ssl_cert: None,
9204 }
9205 }
9206
9207 #[test]
9208 fn test_serve_hashes_v2_piece_layer() {
9209 let hashes: Vec<irontide_core::Id32> = (0..4u8)
9212 .map(|i| {
9213 let mut h = [0u8; 32];
9214 h[0] = i;
9215 irontide_core::Id32(h)
9216 })
9217 .collect();
9218 let file_root = irontide_core::Id32([0xAA; 32]);
9219 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
9220 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
9221
9222 let request = irontide_core::HashRequest {
9223 file_root,
9224 base: 0, index: 0,
9226 count: 4,
9227 proof_layers: 0,
9228 };
9229
9230 let result = serve_hashes(
9231 Some(&meta),
9232 irontide_core::TorrentVersion::V2Only,
9233 Some(&lengths),
9234 &request,
9235 );
9236 let served = result.expect("should serve hashes");
9237 assert_eq!(served.len(), 4);
9238 for (i, h) in served.iter().enumerate() {
9239 assert_eq!(h.0[0], i as u8);
9240 }
9241 }
9242
9243 #[test]
9244 fn test_serve_hashes_rejects_v1_only() {
9245 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
9246 let file_root = irontide_core::Id32([0xAA; 32]);
9247 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
9248 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
9249
9250 let request = irontide_core::HashRequest {
9251 file_root,
9252 base: 0,
9253 index: 0,
9254 count: 1,
9255 proof_layers: 0,
9256 };
9257
9258 let result = serve_hashes(
9259 Some(&meta),
9260 irontide_core::TorrentVersion::V1Only,
9261 Some(&lengths),
9262 &request,
9263 );
9264 assert!(result.is_none(), "V1Only should reject hash requests");
9265 }
9266
9267 #[test]
9268 fn test_serve_hashes_rejects_unknown_root() {
9269 let hashes: Vec<irontide_core::Id32> = vec![irontide_core::Id32([0xBB; 32])];
9270 let file_root = irontide_core::Id32([0xAA; 32]);
9271 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384);
9272 let lengths = Lengths::new(16384, 16384, DEFAULT_CHUNK_SIZE);
9273
9274 let unknown_root = irontide_core::Id32([0xFF; 32]);
9276 let request = irontide_core::HashRequest {
9277 file_root: unknown_root,
9278 base: 0,
9279 index: 0,
9280 count: 1,
9281 proof_layers: 0,
9282 };
9283
9284 let result = serve_hashes(
9285 Some(&meta),
9286 irontide_core::TorrentVersion::V2Only,
9287 Some(&lengths),
9288 &request,
9289 );
9290 assert!(result.is_none(), "unknown file_root should reject");
9291 }
9292
9293 #[test]
9294 fn test_serve_hashes_rejects_out_of_bounds() {
9295 let hashes: Vec<irontide_core::Id32> =
9297 (0..2u8).map(|i| irontide_core::Id32([i; 32])).collect();
9298 let file_root = irontide_core::Id32([0xAA; 32]);
9299 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 2);
9300 let lengths = Lengths::new(16384 * 2, 16384, DEFAULT_CHUNK_SIZE);
9301
9302 let request = irontide_core::HashRequest {
9304 file_root,
9305 base: 0,
9306 index: 5,
9307 count: 1,
9308 proof_layers: 0,
9309 };
9310
9311 let result = serve_hashes(
9312 Some(&meta),
9313 irontide_core::TorrentVersion::V2Only,
9314 Some(&lengths),
9315 &request,
9316 );
9317 assert!(result.is_none(), "out-of-bounds index should reject");
9318 }
9319
9320 #[test]
9321 fn test_serve_hashes_includes_proofs() {
9322 let hashes: Vec<irontide_core::Id32> =
9325 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
9326 let file_root = irontide_core::Id32([0xAA; 32]);
9327 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
9328 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
9329
9330 let request = irontide_core::HashRequest {
9332 file_root,
9333 base: 0,
9334 index: 0,
9335 count: 1,
9336 proof_layers: 1,
9337 };
9338
9339 let result = serve_hashes(
9340 Some(&meta),
9341 irontide_core::TorrentVersion::V2Only,
9342 Some(&lengths),
9343 &request,
9344 );
9345 let served = result.expect("should serve hashes with proofs");
9346 assert_eq!(served.len(), 2, "should have 1 data hash + 1 proof hash");
9348 assert_eq!(served[0], hashes[0]);
9350 assert_eq!(served[1], hashes[1]);
9352 }
9353
9354 #[test]
9355 fn test_serve_hashes_proof_with_batch() {
9356 let hashes: Vec<irontide_core::Id32> =
9371 (0..4u8).map(|i| irontide_core::Id32([i; 32])).collect();
9372 let file_root = irontide_core::Id32([0xAA; 32]);
9373 let meta = make_test_meta_v2(&hashes, file_root, 16384, 16384 * 4);
9374 let lengths = Lengths::new(16384 * 4, 16384, DEFAULT_CHUNK_SIZE);
9375
9376 let request = irontide_core::HashRequest {
9377 file_root,
9378 base: 0,
9379 index: 0,
9380 count: 2,
9381 proof_layers: 1,
9382 };
9383
9384 let result = serve_hashes(
9385 Some(&meta),
9386 irontide_core::TorrentVersion::V2Only,
9387 Some(&lengths),
9388 &request,
9389 );
9390 let served = result.expect("should serve hashes with batch proof");
9391 assert_eq!(served.len(), 3, "should have 2 data hashes + 1 uncle hash");
9393 assert_eq!(served[0], hashes[0]);
9395 assert_eq!(served[1], hashes[1]);
9396 let tree = irontide_core::MerkleTree::from_leaves(&hashes);
9399 let expected_uncle = tree.layer(1)[1]; assert_eq!(served[2], expected_uncle);
9401
9402 let sub_root = irontide_core::MerkleTree::root_from_hashes(&served[..2]);
9405 let uncle_hashes = &served[2..];
9406 let leaf_index = request.index as usize / 2; assert!(
9408 irontide_core::MerkleTree::verify_proof(
9409 tree.root(),
9410 sub_root,
9411 leaf_index,
9412 uncle_hashes
9413 ),
9414 "subtree proof should verify against tree root"
9415 );
9416 }
9417
9418 #[test]
9419 fn is_i2p_synthetic_addr_detects_240_range() {
9420 assert!(is_i2p_synthetic_addr(&"240.0.0.1:1".parse().unwrap()));
9421 assert!(is_i2p_synthetic_addr(
9422 &"255.255.255.255:65535".parse().unwrap()
9423 ));
9424 assert!(!is_i2p_synthetic_addr(&"192.168.1.1:6881".parse().unwrap()));
9425 assert!(!is_i2p_synthetic_addr(&"[::1]:6881".parse().unwrap()));
9426 }
9427
9428 #[tokio::test]
9429 #[ignore = "requires local I2P router with SAM bridge on 127.0.0.1:7656"]
9430 async fn i2p_session_integration() {
9431 let session = crate::i2p::SamSession::create(
9432 "127.0.0.1",
9433 7656,
9434 "integration-test",
9435 crate::i2p::SamTunnelConfig::default(),
9436 )
9437 .await
9438 .expect("SAM session should connect");
9439 assert!(!session.destination().is_empty());
9440 assert!(session.destination().to_b32_address().ends_with(".b32.i2p"));
9441 }
9442
9443 #[test]
9444 fn v6_retry_delay_progression() {
9445 let expected_ms = [100, 200, 400, 800, 1600, 3200, 5000, 5000, 5000, 5000, 5000];
9447 for (count, &expected) in expected_ms.iter().enumerate() {
9448 let delay_ms = {
9449 let base_ms: u64 = 100;
9450 let max_ms: u64 = 5000;
9451 base_ms
9452 .saturating_mul(1u64.checked_shl(count as u32).unwrap_or(u64::MAX))
9453 .min(max_ms)
9454 };
9455 assert_eq!(
9456 delay_ms, expected,
9457 "count={count}: expected {expected}ms, got {delay_ms}ms"
9458 );
9459 }
9460 }
9461
9462 #[test]
9465 fn peer_backoff_exponential() {
9466 let expected_ms: Vec<u64> = vec![400, 800, 1600, 3200, 6400, 12800, 25600, 30000, 30000];
9469 for (i, &expected) in expected_ms.iter().enumerate() {
9470 let attempt = (i as u32) + 1; let delay_ms = 200u64.saturating_mul(1u64 << attempt.min(10)).min(30_000);
9472 assert_eq!(
9473 delay_ms, expected,
9474 "attempt={attempt}: expected {expected}ms, got {delay_ms}ms"
9475 );
9476 }
9477 }
9478
9479 #[test]
9480 fn peer_backoff_clears_on_data() {
9481 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
9484 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
9485
9486 assert!(backoff.get(&addr).is_none());
9488
9489 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
9491 let next = attempt.saturating_add(1);
9492 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
9493 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
9494 backoff.insert(addr, (earliest, next));
9495 assert_eq!(backoff.get(&addr).unwrap().1, 1);
9496
9497 let attempt = backoff.get(&addr).map_or(0, |&(_, a)| a);
9499 let next = attempt.saturating_add(1);
9500 let delay_ms = 200u64.saturating_mul(1u64 << next.min(10)).min(30_000);
9501 let earliest = std::time::Instant::now() + Duration::from_millis(delay_ms);
9502 backoff.insert(addr, (earliest, next));
9503 assert_eq!(backoff.get(&addr).unwrap().1, 2);
9504
9505 backoff.remove(&addr);
9507 assert!(backoff.get(&addr).is_none());
9508 }
9509
9510 #[test]
9511 fn backoff_prevents_hammering() {
9512 let mut backoff: HashMap<SocketAddr, (std::time::Instant, u32)> = HashMap::new();
9514 let addr: SocketAddr = "1.2.3.4:6881".parse().unwrap();
9515
9516 let future = std::time::Instant::now() + Duration::from_secs(10);
9518 backoff.insert(addr, (future, 3));
9519
9520 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
9522 assert!(std::time::Instant::now() < next_attempt);
9523 }
9524
9525 let past = std::time::Instant::now() - Duration::from_secs(1);
9527 backoff.insert(addr, (past, 3));
9528 if let Some(&(next_attempt, _)) = backoff.get(&addr) {
9529 assert!(std::time::Instant::now() >= next_attempt);
9530 }
9531 }
9532
9533 #[test]
9534 fn max_in_flight_formula_updated() {
9535 let formula = |connected: usize, num_pieces: u32| -> usize {
9537 let calculated = 512usize.max(connected.saturating_mul(4));
9538 calculated.min(num_pieces as usize / 2).max(512)
9539 };
9540
9541 assert_eq!(formula(10, 2000), 512);
9543
9544 assert_eq!(formula(200, 2000), 800);
9546
9547 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);
9559
9560 assert_eq!(formula(100, 0), 512);
9562 }
9563
9564 #[test]
9567 fn should_attempt_holepunch_reason_classification() {
9568 assert!(should_attempt_holepunch("connection refused"));
9570 assert!(should_attempt_holepunch("Connection refused"));
9571 assert!(should_attempt_holepunch("timed out"));
9572 assert!(should_attempt_holepunch("Connection reset by peer"));
9573 assert!(should_attempt_holepunch("connection reset by peer"));
9574 assert!(!should_attempt_holepunch(
9576 "holepunch TCP connect failed: Connection refused"
9577 ));
9578 assert!(!should_attempt_holepunch("peer banned"));
9580 assert!(!should_attempt_holepunch("protocol error"));
9581 assert!(!should_attempt_holepunch(""));
9582 }
9583
9584 #[test]
9585 fn holepunch_initiation_on_connect_failure() {
9586 assert!(should_attempt_holepunch("connection refused"));
9588 }
9589
9590 #[test]
9591 fn holepunch_cooldown_prevents_retry() {
9592 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
9593 let addr: SocketAddr = "127.0.0.1:6881".parse().expect("valid test addr");
9594 let now = Instant::now();
9595 cooldowns.insert(addr, now);
9596 assert!(cooldowns.contains_key(&addr));
9598 }
9599
9600 #[test]
9601 fn holepunch_cooldown_overflow_skips() {
9602 let mut cooldowns: HashMap<SocketAddr, Instant> = HashMap::new();
9603 let now = Instant::now();
9604 for i in 0..256u16 {
9605 let addr: SocketAddr = format!("10.0.{}.{}:6881", i / 256, i % 256)
9606 .parse()
9607 .expect("valid test addr");
9608 cooldowns.insert(addr, now);
9609 }
9610 assert_eq!(cooldowns.len(), HOLEPUNCH_MAX_TRACKED);
9611 }
9613
9614 #[test]
9615 fn holepunch_skipped_when_disabled() {
9616 assert!(should_attempt_holepunch("connection refused"));
9619 }
9621
9622 #[test]
9623 fn holepunch_not_triggered_on_ban() {
9624 assert!(!should_attempt_holepunch("peer banned"));
9625 assert!(!should_attempt_holepunch("banned for bad data"));
9626 }
9627
9628 fn make_multi_file_meta(files: Vec<(u64, &str)>, piece_length: u64) -> TorrentMetaV1 {
9632 let total_length: u64 = files.iter().map(|(len, _)| *len).sum();
9633 let num_pieces = total_length.div_ceil(piece_length) as usize;
9634 let file_entries: Vec<irontide_core::FileEntry> = files
9635 .iter()
9636 .map(|(length, name)| irontide_core::FileEntry {
9637 length: *length,
9638 path: vec![name.to_string()],
9639 attr: None,
9640 mtime: None,
9641 symlink_path: None,
9642 })
9643 .collect();
9644 TorrentMetaV1 {
9645 info_hash: Id20([0u8; 20]),
9646 announce: None,
9647 announce_list: None,
9648 comment: None,
9649 created_by: None,
9650 creation_date: None,
9651 info: irontide_core::InfoDict {
9652 name: "test".to_string(),
9653 piece_length,
9654 pieces: vec![0u8; num_pieces * 20],
9655 length: None,
9656 files: Some(file_entries),
9657 private: None,
9658 source: None,
9659 ssl_cert: None,
9660 similar: Vec::new(),
9661 collections: Vec::new(),
9662 },
9663 url_list: Vec::new(),
9664 httpseeds: Vec::new(),
9665 info_bytes: None,
9666 ssl_cert: None,
9667 }
9668 }
9669
9670 #[test]
9671 fn cached_files_populated_on_registration() {
9672 let meta = make_multi_file_meta(vec![(100, "a.txt"), (200, "b.txt"), (50, "c.txt")], 100);
9678 let lengths = Lengths::new(350, 100, 16384);
9679 let cached = build_cached_file_info(&meta, &lengths);
9680
9681 assert_eq!(cached.entries.len(), 3);
9682
9683 assert_eq!(cached.entries[0].index, 0);
9684 assert_eq!(cached.entries[0].length, 100);
9685 assert_eq!(cached.entries[0].first_piece, 0);
9686 assert_eq!(cached.entries[0].last_piece, 0);
9687
9688 assert_eq!(cached.entries[1].index, 1);
9689 assert_eq!(cached.entries[1].length, 200);
9690 assert_eq!(cached.entries[1].first_piece, 1);
9691 assert_eq!(cached.entries[1].last_piece, 2);
9692
9693 assert_eq!(cached.entries[2].index, 2);
9694 assert_eq!(cached.entries[2].length, 50);
9695 assert_eq!(cached.entries[2].first_piece, 3);
9696 assert_eq!(cached.entries[2].last_piece, 3);
9697 }
9698
9699 #[test]
9700 fn cached_files_single_file_torrent() {
9701 let meta = TorrentMetaV1 {
9704 info_hash: Id20([0u8; 20]),
9705 announce: None,
9706 announce_list: None,
9707 comment: None,
9708 created_by: None,
9709 creation_date: None,
9710 info: irontide_core::InfoDict {
9711 name: "single.bin".to_string(),
9712 piece_length: 100,
9713 pieces: vec![0u8; 5 * 20],
9714 length: Some(500),
9715 files: None,
9716 private: None,
9717 source: None,
9718 ssl_cert: None,
9719 similar: Vec::new(),
9720 collections: Vec::new(),
9721 },
9722 url_list: Vec::new(),
9723 httpseeds: Vec::new(),
9724 info_bytes: None,
9725 ssl_cert: None,
9726 };
9727 let lengths = Lengths::new(500, 100, 16384);
9728 let cached = build_cached_file_info(&meta, &lengths);
9729
9730 assert_eq!(cached.entries.len(), 1);
9731 assert_eq!(cached.entries[0].index, 0);
9732 assert_eq!(cached.entries[0].length, 500);
9733 assert_eq!(cached.entries[0].first_piece, 0);
9734 assert_eq!(cached.entries[0].last_piece, 4);
9735 }
9736
9737 use crate::piece_reservation::{AtomicPieceStates, PieceState, StealCandidates};
9744 use irontide_storage::Bitfield;
9745
9746 fn steal_populate_scan(states: &AtomicPieceStates, sc: &StealCandidates) -> u32 {
9750 let mut pushed = 0u32;
9751 let num = states.len();
9752 for piece in 0..num {
9753 let state = states.get(piece);
9754 if state == PieceState::Reserved {
9755 sc.push(piece);
9756 pushed = pushed.saturating_add(1);
9757 }
9758 }
9759 pushed
9760 }
9761
9762 fn all_wanted(n: u32) -> Bitfield {
9763 let mut bf = Bitfield::new(n);
9764 for i in 0..n {
9765 bf.set(i);
9766 }
9767 bf
9768 }
9769
9770 #[test]
9771 fn steal_populate_pushes_reserved_pieces() {
9772 let n = 10;
9773 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
9774 let sc = StealCandidates::new();
9775
9776 assert!(states.try_reserve(2));
9778 assert!(states.try_reserve(5));
9779 assert!(states.try_reserve(7));
9780
9781 let pushed = steal_populate_scan(&states, &sc);
9782 assert_eq!(pushed, 3, "should push exactly the 3 reserved pieces");
9783
9784 let mut popped = Vec::new();
9786 while let Some(p) = sc.pop() {
9787 popped.push(p);
9788 }
9789 popped.sort();
9790 assert_eq!(popped, vec![2, 5, 7]);
9791 }
9792
9793 #[test]
9794 fn steal_populate_skips_non_reserved_states() {
9795 let n = 8;
9796 let mut have = Bitfield::new(n);
9797 have.set(0); let mut wanted = all_wanted(n);
9799 wanted.clear(1); let states = AtomicPieceStates::new(n, &have, &wanted);
9802 let sc = StealCandidates::new();
9803
9804 assert!(states.try_reserve(3));
9806
9807 let pushed = steal_populate_scan(&states, &sc);
9808 assert_eq!(pushed, 1, "only piece 3 (Reserved) should be pushed");
9809
9810 assert_eq!(sc.pop(), Some(3));
9811 assert_eq!(sc.pop(), None);
9812 }
9813
9814 #[test]
9815 fn steal_populate_deduplicates() {
9816 let n = 4;
9817 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
9818 let sc = StealCandidates::new();
9819
9820 assert!(states.try_reserve(1));
9821 assert!(states.try_reserve(2));
9822
9823 let pushed1 = steal_populate_scan(&states, &sc);
9825 assert_eq!(pushed1, 2);
9826
9827 let pushed2 = steal_populate_scan(&states, &sc);
9830 assert_eq!(pushed2, 2, "scan still reports 2 reserved pieces");
9831
9832 let mut count = 0u32;
9833 while sc.pop().is_some() {
9834 count = count.saturating_add(1);
9835 }
9836 assert_eq!(count, 2, "dedup means only 2 entries despite 2 scans");
9837 }
9838
9839 #[test]
9840 fn steal_populate_skips_completed_pieces() {
9841 let n = 5;
9842 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
9843 let sc = StealCandidates::new();
9844
9845 for i in 0..n {
9847 assert!(states.try_reserve(i));
9848 }
9849
9850 states.mark_complete(1);
9852 states.mark_complete(3);
9853
9854 let pushed = steal_populate_scan(&states, &sc);
9855 assert_eq!(pushed, 3, "3 pieces still Reserved (0, 2, 4)");
9856
9857 let mut popped = Vec::new();
9858 while let Some(p) = sc.pop() {
9859 popped.push(p);
9860 }
9861 popped.sort();
9862 assert_eq!(popped, vec![0, 2, 4]);
9863 }
9864
9865 #[test]
9866 fn steal_populate_empty_when_no_reserved() {
9867 let n = 6;
9868 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
9869 let sc = StealCandidates::new();
9870
9871 let pushed = steal_populate_scan(&states, &sc);
9873 assert_eq!(pushed, 0);
9874 assert_eq!(sc.pop(), None);
9875 }
9876
9877 #[test]
9878 fn steal_populate_with_endgame_pieces() {
9879 let n = 4;
9881 let states = AtomicPieceStates::new(n, &Bitfield::new(n), &all_wanted(n));
9882 let sc = StealCandidates::new();
9883
9884 assert!(states.try_reserve(0));
9885 assert!(states.try_reserve(1));
9886 states.transition_to_endgame(1);
9887
9888 let pushed = steal_populate_scan(&states, &sc);
9889 assert_eq!(
9890 pushed, 1,
9891 "only piece 0 (Reserved) should be pushed, not piece 1 (Endgame)"
9892 );
9893 assert_eq!(sc.pop(), Some(0));
9894 assert_eq!(sc.pop(), None);
9895 }
9896
9897 #[test]
9906 fn dht_requery_guard_scales_with_max_peers() {
9907 assert_eq!(128_usize.saturating_mul(4), 512);
9909
9910 assert_eq!(200_usize.saturating_mul(4), 800);
9912
9913 assert_eq!(50_usize.saturating_mul(4), 200);
9915
9916 assert_eq!(usize::MAX.saturating_mul(4), usize::MAX);
9918 }
9919
9920 fn make_test_info_bytes() -> (Vec<u8>, Id20) {
9924 use serde::Serialize;
9925
9926 let data = vec![0xAB; 1024];
9927 let piece_hash = irontide_core::sha1(&data);
9928 let mut pieces = Vec::new();
9929 pieces.extend_from_slice(piece_hash.as_bytes());
9930
9931 #[derive(Serialize)]
9932 struct Info<'a> {
9933 length: u64,
9934 name: &'a str,
9935 #[serde(rename = "piece length")]
9936 piece_length: u64,
9937 #[serde(with = "serde_bytes")]
9938 pieces: &'a [u8],
9939 }
9940
9941 let info = Info {
9942 length: 1024,
9943 name: "test",
9944 piece_length: 16384,
9945 pieces: &pieces,
9946 };
9947
9948 let info_bytes = irontide_bencode::to_bytes(&info).unwrap();
9949 let info_hash = irontide_core::sha1(&info_bytes);
9950 (info_bytes, info_hash)
9951 }
9952
9953 async fn create_magnet_handle(info_hash: Id20) -> TorrentHandle {
9955 let magnet = Magnet {
9956 info_hashes: irontide_core::InfoHashes::v1_only(info_hash),
9957 display_name: Some("test".into()),
9958 trackers: vec![],
9959 peers: vec![],
9960 selected_files: None,
9961 };
9962 let config = test_config();
9963 let (atx, amask) = test_alert_channel();
9964 let (dm, _dj) = test_disk_manager();
9965 TorrentHandle::from_magnet(
9966 magnet,
9967 dm,
9968 config,
9969 None,
9970 None,
9971 None,
9972 None,
9973 crate::slot_tuner::SlotTuner::disabled(4),
9974 atx,
9975 amask,
9976 None,
9977 None,
9978 test_ban_manager(),
9979 test_ip_filter(),
9980 Arc::new(Vec::new()),
9981 None,
9982 None,
9983 Arc::new(crate::transport::NetworkFactory::tokio()),
9984 None,
9985 )
9986 .await
9987 .unwrap()
9988 }
9989
9990 #[tokio::test]
9991 async fn pre_resolved_metadata_applies_when_fetching() {
9992 let (info_bytes, info_hash) = make_test_info_bytes();
9993 let handle = create_magnet_handle(info_hash).await;
9994
9995 let stats = handle.stats().await.unwrap();
9997 assert_eq!(stats.state, TorrentState::FetchingMetadata);
9998
9999 let peer_addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
10001 handle.send_pre_resolved_metadata(info_bytes, vec![peer_addr]);
10002
10003 tokio::time::sleep(Duration::from_millis(200)).await;
10005
10006 let stats = handle.stats().await.unwrap();
10008 assert_eq!(
10009 stats.state,
10010 TorrentState::Downloading,
10011 "should have transitioned to Downloading after pre-resolved metadata"
10012 );
10013 assert!(
10014 stats.pieces_total > 0,
10015 "should know piece count after metadata resolution"
10016 );
10017
10018 handle.shutdown().await.unwrap();
10019 }
10020
10021 #[tokio::test]
10022 async fn pre_resolved_metadata_ignored_after_resolution() {
10023 let data = vec![0xAB; 32768];
10025 let meta = make_test_torrent(&data, 16384);
10026 let info_hash = meta.info_hash;
10027 let storage = make_storage(&data, 16384);
10028 let config = test_config();
10029
10030 let (atx, amask) = test_alert_channel();
10031 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
10032 let handle = TorrentHandle::from_torrent(
10033 meta,
10034 irontide_core::TorrentVersion::V1Only,
10035 None,
10036 dh,
10037 dm,
10038 config,
10039 None,
10040 None,
10041 None,
10042 None,
10043 crate::slot_tuner::SlotTuner::disabled(4),
10044 atx,
10045 amask,
10046 None,
10047 None,
10048 test_ban_manager(),
10049 test_ip_filter(),
10050 Arc::new(Vec::new()),
10051 None,
10052 None,
10053 Arc::new(crate::transport::NetworkFactory::tokio()),
10054 None,
10055 )
10056 .await
10057 .unwrap();
10058
10059 let stats_before = handle.stats().await.unwrap();
10060 assert_eq!(stats_before.state, TorrentState::Downloading);
10061
10062 let (info_bytes, _) = make_test_info_bytes();
10065 handle.send_pre_resolved_metadata(info_bytes, vec![]);
10066
10067 tokio::time::sleep(Duration::from_millis(100)).await;
10069
10070 let stats_after = handle.stats().await.unwrap();
10072 assert_eq!(stats_after.state, TorrentState::Downloading);
10073 assert_eq!(stats_after.pieces_total, stats_before.pieces_total);
10074
10075 handle.shutdown().await.unwrap();
10076 }
10077
10078 #[tokio::test]
10079 async fn pre_resolved_metadata_with_invalid_hash_stays_fetching() {
10080 let (info_bytes, _correct_hash) = make_test_info_bytes();
10084
10085 let wrong_hash = Id20::from_hex("0000000000000000000000000000000000000001").unwrap();
10087 let handle = create_magnet_handle(wrong_hash).await;
10088
10089 let stats = handle.stats().await.unwrap();
10090 assert_eq!(stats.state, TorrentState::FetchingMetadata);
10091
10092 handle.send_pre_resolved_metadata(info_bytes, vec![]);
10094
10095 tokio::time::sleep(Duration::from_millis(200)).await;
10096
10097 let stats = handle.stats().await.unwrap();
10099 assert_eq!(
10100 stats.state,
10101 TorrentState::FetchingMetadata,
10102 "should stay in FetchingMetadata when info_hash doesn't match"
10103 );
10104
10105 handle.shutdown().await.unwrap();
10106 }
10107
10108 fn compute_pipeline_depth(
10112 download_rate: u64,
10113 target_buffer_secs: f64,
10114 min_depth: u32,
10115 max_depth: u32,
10116 ) -> u32 {
10117 let target = ((download_rate as f64 / 16384.0) * target_buffer_secs) as u32;
10118 target.clamp(min_depth, max_depth)
10119 }
10120
10121 #[test]
10122 fn m149_pipeline_depth_fast_peer_clamped_to_max() {
10123 let depth = compute_pipeline_depth(5_400_000, 2.0, 16, 512);
10125 assert_eq!(depth, 512);
10126 }
10127
10128 #[test]
10129 fn m149_pipeline_depth_slow_peer_clamped_to_min() {
10130 let depth = compute_pipeline_depth(100_000, 2.0, 16, 512);
10132 assert_eq!(depth, 16);
10133 }
10134
10135 #[test]
10136 fn m149_pipeline_depth_medium_peer() {
10137 let depth = compute_pipeline_depth(500_000, 2.0, 16, 512);
10139 assert_eq!(depth, 61);
10140 }
10141
10142 #[test]
10143 fn m149_pipeline_depth_zero_rate() {
10144 let depth = compute_pipeline_depth(0, 2.0, 16, 512);
10146 assert_eq!(depth, 16);
10147 }
10148
10149 #[test]
10150 fn m149_pipeline_depth_initial_value() {
10151 use crate::peer_shared::INITIAL_QUEUE_DEPTH;
10153 assert_eq!(INITIAL_QUEUE_DEPTH, 128);
10154 }
10155
10156 #[tokio::test]
10169 async fn m159_seed_mode_suppresses_new_requests_on_wire() {
10170 let data = vec![0xAB; 32768]; let meta = make_test_torrent(&data, 16384); let info_hash = meta.info_hash;
10173 let storage = make_storage(&[0u8; 32768], 16384);
10175
10176 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
10177 let listen_addr = listener.local_addr().unwrap();
10178 let config = TorrentConfig {
10179 listen_port: listen_addr.port(),
10180 ..test_config()
10181 };
10182 drop(listener);
10183
10184 let (atx, amask) = test_alert_channel();
10185 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
10186 let handle = TorrentHandle::from_torrent(
10187 meta,
10188 irontide_core::TorrentVersion::V1Only,
10189 None,
10190 dh,
10191 dm,
10192 config,
10193 None,
10194 None,
10195 None,
10196 None,
10197 crate::slot_tuner::SlotTuner::disabled(4),
10198 atx,
10199 amask,
10200 None,
10201 None,
10202 test_ban_manager(),
10203 test_ip_filter(),
10204 Arc::new(Vec::new()),
10205 None,
10206 None,
10207 Arc::new(crate::transport::NetworkFactory::tokio()),
10208 None,
10209 )
10210 .await
10211 .unwrap();
10212
10213 tokio::time::sleep(Duration::from_millis(50)).await;
10214
10215 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
10217 let (reader, writer) = tokio::io::split(stream);
10218 let mut writer = writer;
10219 let mut reader = reader;
10220
10221 let hs = Handshake::new(
10222 info_hash,
10223 Id20::from_hex("dddddddddddddddddddddddddddddddddddddddd").unwrap(),
10224 );
10225 writer.write_all(&hs.to_bytes()).await.unwrap();
10226 writer.flush().await.unwrap();
10227 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
10228 reader.read_exact(&mut hs_buf).await.unwrap();
10229
10230 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
10231 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
10232
10233 let _actor_ext_hs = framed_read.next().await;
10235 let ext_hs = ExtHandshake::new();
10236 let ext_payload = ext_hs.to_bytes().unwrap();
10237 framed_write
10238 .send(Message::Extended {
10239 ext_id: 0,
10240 payload: ext_payload,
10241 })
10242 .await
10243 .unwrap();
10244
10245 let mut bf = Bitfield::new(2);
10247 bf.set(0);
10248 bf.set(1);
10249 framed_write
10250 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
10251 .await
10252 .unwrap();
10253 framed_write.send(Message::Unchoke).await.unwrap();
10254
10255 let mut initial_request_seen = false;
10259 let wait_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
10260 loop {
10261 let remaining = wait_deadline.saturating_duration_since(tokio::time::Instant::now());
10262 if remaining.is_zero() {
10263 break;
10264 }
10265 match tokio::time::timeout(remaining, framed_read.next()).await {
10266 Ok(Some(Ok(Message::Request { .. }))) => {
10267 initial_request_seen = true;
10268 break;
10269 }
10270 Ok(Some(Ok(_))) => continue,
10271 _ => break,
10272 }
10273 }
10274 assert!(
10275 initial_request_seen,
10276 "actor should have sent a Request before seed mode toggle"
10277 );
10278
10279 handle.set_seed_mode(true).await.unwrap();
10282
10283 let grace_deadline = tokio::time::Instant::now() + Duration::from_millis(200);
10290 let mut cancel_seen = false;
10291 let mut grace_requests = 0u32;
10292 loop {
10293 let remaining = grace_deadline.saturating_duration_since(tokio::time::Instant::now());
10294 if remaining.is_zero() {
10295 break;
10296 }
10297 match tokio::time::timeout(remaining, framed_read.next()).await {
10298 Ok(Some(Ok(Message::Request { .. }))) => {
10299 grace_requests += 1;
10300 }
10301 Ok(Some(Ok(Message::Cancel { .. }))) => {
10302 cancel_seen = true;
10303 }
10304 Ok(Some(Ok(_))) => continue,
10305 Ok(None) => break,
10306 Ok(Some(Err(_))) => break,
10307 Err(_elapsed) => break,
10308 }
10309 }
10310 let _ = (cancel_seen, grace_requests);
10311
10312 let steady_deadline = tokio::time::Instant::now() + Duration::from_millis(500);
10315 let mut steady_requests = 0u32;
10316 loop {
10317 let remaining = steady_deadline.saturating_duration_since(tokio::time::Instant::now());
10318 if remaining.is_zero() {
10319 break;
10320 }
10321 match tokio::time::timeout(remaining, framed_read.next()).await {
10322 Ok(Some(Ok(Message::Request { .. }))) => {
10323 steady_requests += 1;
10324 }
10325 Ok(Some(Ok(_))) => continue,
10326 Ok(None) => break,
10327 Ok(Some(Err(_))) => break,
10328 Err(_elapsed) => break,
10329 }
10330 }
10331
10332 assert_eq!(
10333 steady_requests, 0,
10334 "after the Stop propagation grace window, no new Request messages \
10335 must appear during steady-state while user_seed_mode is active"
10336 );
10337
10338 let stats = handle.stats().await.unwrap();
10340 assert!(
10341 stats.user_seed_mode,
10342 "stats.user_seed_mode should be true after set_seed_mode(true)"
10343 );
10344
10345 handle.shutdown().await.unwrap();
10346 }
10347
10348 #[tokio::test]
10376 async fn m159_seed_mode_uploads_continue_on_wire() {
10377 const FILL_BYTE: u8 = 0x5A;
10378 const PIECE_LENGTH: u64 = 16384;
10379 const TOTAL_LEN: usize = 32768; let data = vec![FILL_BYTE; TOTAL_LEN];
10382 let meta = make_test_torrent(&data, PIECE_LENGTH);
10383 let info_hash = meta.info_hash;
10384 let storage = make_seeded_storage(&data, PIECE_LENGTH);
10386
10387 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
10388 let listen_addr = listener.local_addr().unwrap();
10389 let config = TorrentConfig {
10390 listen_port: listen_addr.port(),
10391 ..test_config()
10392 };
10393 drop(listener);
10394
10395 let (atx, amask) = test_alert_channel();
10396 let (dh, dm, _dj) = test_register_disk(info_hash, storage).await;
10397 let handle = TorrentHandle::from_torrent(
10398 meta,
10399 irontide_core::TorrentVersion::V1Only,
10400 None,
10401 dh,
10402 dm,
10403 config,
10404 None,
10405 None,
10406 None,
10407 None,
10408 crate::slot_tuner::SlotTuner::disabled(4),
10409 atx,
10410 amask,
10411 None,
10412 None,
10413 test_ban_manager(),
10414 test_ip_filter(),
10415 Arc::new(Vec::new()),
10416 None,
10417 None,
10418 Arc::new(crate::transport::NetworkFactory::tokio()),
10419 None,
10420 )
10421 .await
10422 .unwrap();
10423
10424 let seeding_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
10427 loop {
10428 tokio::time::sleep(Duration::from_millis(50)).await;
10429 let stats = handle.stats().await.unwrap();
10430 if stats.state == TorrentState::Seeding && stats.pieces_have == 2 {
10431 break;
10432 }
10433 if tokio::time::Instant::now() > seeding_deadline {
10434 let stats = handle.stats().await.unwrap();
10435 panic!(
10436 "actor did not reach Seeding state within 3s: state={:?}, have={}/{}",
10437 stats.state, stats.pieces_have, stats.pieces_total
10438 );
10439 }
10440 }
10441
10442 handle.set_seed_mode(true).await.unwrap();
10445 let stats = handle.stats().await.unwrap();
10446 assert!(
10447 stats.user_seed_mode,
10448 "stats.user_seed_mode should be true after set_seed_mode(true)"
10449 );
10450
10451 let stream = tokio::net::TcpStream::connect(listen_addr).await.unwrap();
10453 let (reader, writer) = tokio::io::split(stream);
10454 let mut writer = writer;
10455 let mut reader = reader;
10456
10457 let hs = Handshake::new(
10458 info_hash,
10459 Id20::from_hex("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").unwrap(),
10460 );
10461 writer.write_all(&hs.to_bytes()).await.unwrap();
10462 writer.flush().await.unwrap();
10463 let mut hs_buf = [0u8; HANDSHAKE_SIZE];
10464 reader.read_exact(&mut hs_buf).await.unwrap();
10465
10466 let mut framed_read = FramedRead::new(reader, MessageCodec::new());
10467 let mut framed_write = FramedWrite::new(writer, MessageCodec::new());
10468
10469 let _actor_ext_hs = framed_read.next().await;
10471 let ext_hs = ExtHandshake::new();
10472 let ext_payload = ext_hs.to_bytes().unwrap();
10473 framed_write
10474 .send(Message::Extended {
10475 ext_id: 0,
10476 payload: ext_payload,
10477 })
10478 .await
10479 .unwrap();
10480
10481 let bf = Bitfield::new(2);
10483 framed_write
10484 .send(Message::Bitfield(Bytes::copy_from_slice(bf.as_bytes())))
10485 .await
10486 .unwrap();
10487 framed_write.send(Message::Interested).await.unwrap();
10488
10489 let unchoke_deadline = tokio::time::Instant::now() + Duration::from_secs(3);
10493 let mut saw_unchoke = false;
10494 loop {
10495 let remaining = unchoke_deadline.saturating_duration_since(tokio::time::Instant::now());
10496 if remaining.is_zero() {
10497 break;
10498 }
10499 match tokio::time::timeout(remaining, framed_read.next()).await {
10500 Ok(Some(Ok(Message::Unchoke))) => {
10501 saw_unchoke = true;
10502 break;
10503 }
10504 Ok(Some(Ok(_))) => continue,
10505 Ok(None) | Ok(Some(Err(_))) => break,
10506 Err(_elapsed) => break,
10507 }
10508 }
10509 assert!(
10510 saw_unchoke,
10511 "actor should have unchoked the leecher while user_seed_mode is active"
10512 );
10513
10514 framed_write
10517 .send(Message::Request {
10518 index: 0,
10519 begin: 0,
10520 length: PIECE_LENGTH as u32,
10521 })
10522 .await
10523 .unwrap();
10524
10525 let piece_deadline = tokio::time::Instant::now() + Duration::from_secs(2);
10530 let mut got_piece = false;
10531 loop {
10532 let remaining = piece_deadline.saturating_duration_since(tokio::time::Instant::now());
10533 if remaining.is_zero() {
10534 break;
10535 }
10536 match tokio::time::timeout(remaining, framed_read.next()).await {
10537 Ok(Some(Ok(Message::Piece {
10538 index,
10539 begin,
10540 data_0,
10541 data_1,
10542 }))) => {
10543 assert_eq!(index, 0, "Piece index should match request");
10544 assert_eq!(begin, 0, "Piece begin should match request");
10545 let mut payload: Vec<u8> =
10546 Vec::with_capacity(data_0.len().saturating_add(data_1.len()));
10547 payload.extend_from_slice(&data_0);
10548 payload.extend_from_slice(&data_1);
10549 assert_eq!(
10550 payload.len(),
10551 PIECE_LENGTH as usize,
10552 "Piece payload length should match requested length"
10553 );
10554 assert!(
10555 payload.iter().all(|&b| b == FILL_BYTE),
10556 "Piece payload should contain the pre-seeded fill byte"
10557 );
10558 got_piece = true;
10559 break;
10560 }
10561 Ok(Some(Ok(_))) => continue,
10562 Ok(None) | Ok(Some(Err(_))) => break,
10563 Err(_elapsed) => break,
10564 }
10565 }
10566 assert!(
10567 got_piece,
10568 "actor should have served a Piece in response to Request while user_seed_mode is active"
10569 );
10570
10571 let stats = handle.stats().await.unwrap();
10574 assert!(
10575 stats.user_seed_mode,
10576 "stats.user_seed_mode should remain true after serving an upload"
10577 );
10578 assert!(
10579 stats.uploaded >= u64::from(PIECE_LENGTH as u32),
10580 "stats.uploaded should reflect the served block, got {}",
10581 stats.uploaded
10582 );
10583
10584 handle.shutdown().await.unwrap();
10585 }
10586
10587 #[tokio::test]
10590 async fn info_field_populated_for_torrent() {
10591 let data = vec![0xAB; 32768];
10592 let meta = make_test_torrent(&data, 16384);
10593 let storage = make_storage(&data, 16384);
10594 let config = test_config();
10595
10596 let (atx, amask) = test_alert_channel();
10597 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10598 let handle = TorrentHandle::from_torrent(
10599 meta,
10600 irontide_core::TorrentVersion::V1Only,
10601 None,
10602 dh,
10603 dm,
10604 config,
10605 None,
10606 None,
10607 None,
10608 None,
10609 crate::slot_tuner::SlotTuner::disabled(4),
10610 atx,
10611 amask,
10612 None,
10613 None,
10614 test_ban_manager(),
10615 test_ip_filter(),
10616 Arc::new(Vec::new()),
10617 None,
10618 None,
10619 Arc::new(crate::transport::NetworkFactory::tokio()),
10620 None,
10621 )
10622 .await
10623 .unwrap();
10624
10625 tokio::time::sleep(Duration::from_millis(50)).await;
10626
10627 let rd = handle.save_resume_data().await.unwrap();
10628
10629 assert!(rd.info.is_some(), "rd.info should be Some for .torrent");
10631
10632 let info_bytes = rd.info.as_ref().unwrap();
10634 let info: irontide_core::InfoDict =
10635 irontide_bencode::from_bytes(info_bytes).expect("info bytes should deserialize");
10636 assert_eq!(info.name, "test");
10637 assert_eq!(info.piece_length, 16384);
10638
10639 handle.shutdown().await.unwrap();
10640 }
10641
10642 #[tokio::test]
10643 async fn info_hash2_none_for_v1_only() {
10644 let data = vec![0xCD; 16384];
10645 let meta = make_test_torrent(&data, 16384);
10646 let storage = make_storage(&data, 16384);
10647 let config = test_config();
10648
10649 let (atx, amask) = test_alert_channel();
10650 let (dh, dm, _dj) = test_register_disk(meta.info_hash, storage).await;
10651 let handle = TorrentHandle::from_torrent(
10652 meta,
10653 irontide_core::TorrentVersion::V1Only,
10654 None,
10655 dh,
10656 dm,
10657 config,
10658 None,
10659 None,
10660 None,
10661 None,
10662 crate::slot_tuner::SlotTuner::disabled(4),
10663 atx,
10664 amask,
10665 None,
10666 None,
10667 test_ban_manager(),
10668 test_ip_filter(),
10669 Arc::new(Vec::new()),
10670 None,
10671 None,
10672 Arc::new(crate::transport::NetworkFactory::tokio()),
10673 None,
10674 )
10675 .await
10676 .unwrap();
10677
10678 tokio::time::sleep(Duration::from_millis(50)).await;
10679
10680 let rd = handle.save_resume_data().await.unwrap();
10681
10682 assert!(
10684 rd.info_hash2.is_none(),
10685 "v1-only torrent should have info_hash2 = None"
10686 );
10687
10688 assert!(
10690 rd.added_time > 0,
10691 "added_time should be a positive POSIX timestamp"
10692 );
10693
10694 handle.shutdown().await.unwrap();
10695 }
10696
10697 #[tokio::test]
10698 async fn info_none_for_unresolved_magnet() {
10699 let magnet = Magnet {
10700 info_hashes: irontide_core::InfoHashes::v1_only(
10701 Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
10702 ),
10703 display_name: Some("magnet-test".into()),
10704 trackers: vec![],
10705 peers: vec![],
10706 selected_files: None,
10707 };
10708 let config = test_config();
10709
10710 let (atx, amask) = test_alert_channel();
10711 let (dm, _dj) = test_disk_manager();
10712 let handle = TorrentHandle::from_magnet(
10713 magnet,
10714 dm,
10715 config,
10716 None,
10717 None,
10718 None,
10719 None,
10720 crate::slot_tuner::SlotTuner::disabled(4),
10721 atx,
10722 amask,
10723 None,
10724 None,
10725 test_ban_manager(),
10726 test_ip_filter(),
10727 Arc::new(Vec::new()),
10728 None,
10729 None,
10730 Arc::new(crate::transport::NetworkFactory::tokio()),
10731 None,
10732 )
10733 .await
10734 .unwrap();
10735
10736 tokio::time::sleep(Duration::from_millis(50)).await;
10737
10738 let rd = handle.save_resume_data().await.unwrap();
10739
10740 assert!(
10742 rd.info.is_none(),
10743 "unresolved magnet should have info = None"
10744 );
10745
10746 assert!(
10748 rd.added_time > 0,
10749 "added_time should be set for magnet links"
10750 );
10751
10752 handle.shutdown().await.unwrap();
10753 }
10754}