1use std::collections::HashMap;
2use std::net::SocketAddr;
3use std::path::PathBuf;
4
5use bitflags::bitflags;
6use bytes::Bytes;
7use serde::{Deserialize, Serialize};
8use tokio::sync::oneshot;
9
10use irontide_storage::Bitfield;
11use irontide_wire::ExtHandshake;
12
13use crate::choker::{ChokingAlgorithm, SeedChokingAlgorithm};
14
15#[derive(Debug, Clone)]
17pub struct TorrentConfig {
18 pub listen_port: u16,
20 pub max_peers: usize,
22 pub target_request_queue: usize,
24 pub download_dir: PathBuf,
26 pub enable_dht: bool,
28 pub enable_pex: bool,
30 pub enable_fast: bool,
32 pub seed_ratio_limit: Option<f64>,
34 pub seed_time_limit_secs: Option<u64>,
37 pub inactive_seed_time_limit_secs: Option<u64>,
41 pub strict_end_game: bool,
43 pub upload_rate_limit: u64,
45 pub download_rate_limit: u64,
47 pub max_uploads_per_torrent: i32,
52 pub encryption_mode: irontide_wire::mse::EncryptionMode,
54 pub enable_utp: bool,
56 pub enable_web_seed: bool,
58 pub enable_holepunch: bool,
60 pub enable_bep40_eviction: bool,
62 pub max_web_seeds: usize,
64 pub web_seed_retry_base_secs: u64,
66 pub web_seed_retry_factor: u64,
68 pub web_seed_retry_cap_secs: u64,
70 pub web_seed_max_failures: u32,
72 pub super_seeding: bool,
74 pub upload_only_announce: bool,
76 pub hashing_threads: usize,
78 pub sequential_download: bool,
80 pub initial_picker_threshold: u32,
82 pub whole_pieces_threshold: u32,
85 pub snub_timeout_secs: u32,
87 pub readahead_pieces: u32,
89 pub streaming_timeout_escalation: bool,
91 pub max_concurrent_stream_reads: usize,
93 pub proxy: crate::proxy::ProxyConfig,
95 pub anonymous_mode: bool,
97 pub share_mode: bool,
100 pub enable_i2p: bool,
102 pub allow_i2p_mixed: bool,
104 pub ssl_listen_port: u16,
106 pub seed_choking_algorithm: SeedChokingAlgorithm,
108 pub choking_algorithm: ChokingAlgorithm,
110 pub piece_extent_affinity: bool,
112 pub suggest_mode: bool,
114 pub max_suggest_pieces: usize,
116 pub predictive_piece_announce_ms: u64,
118 pub mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm,
120 pub auto_sequential: bool,
122 pub storage_mode: irontide_core::StorageMode,
124 pub preallocate_mode: Option<irontide_storage::PreallocateMode>,
126 pub block_request_timeout_secs: u32,
128 pub enable_lsd: bool,
130 pub force_proxy: bool,
132 pub steal_threshold_ratio: f64,
134 pub steal_threshold_endgame: f64,
136 pub peer_read_timeout_secs: u64,
138 pub peer_write_timeout_secs: u64,
140 pub data_contribution_timeout_secs: u64,
142 pub pass0_grace_secs: u64,
145 pub proactive_evictions_per_minute_limit: u32,
148 pub eviction_ban_duration_secs: u64,
150 pub eviction_ban_set_cap: usize,
152 pub choke_rotation_max_evictions: u32,
154 pub max_concurrent_connects: u16,
156 pub connect_soft_timeout: u64,
158 pub dispatch_backlog_cap: usize,
160 pub event_backlog_cap: usize,
162 pub use_actor_dispatch: bool,
164 pub web_seed_progress_throttle_ms: u64,
169 pub url_security: crate::url_guard::UrlSecurityConfig,
171 pub peer_connect_timeout: u64,
173 pub peer_dscp: u8,
175 pub initial_queue_depth: usize,
177 pub max_request_queue_depth: usize,
179 pub request_queue_time: f64,
182 pub max_metadata_size: u64,
184 pub max_message_size: usize,
186 pub max_piece_length: u64,
188 pub max_outstanding_requests: usize,
190 pub max_in_flight_pieces: usize,
193 pub use_block_stealing: bool,
195 pub steal_stale_piece_secs: u64,
197 pub fixed_pipeline_depth: usize,
199 pub lock_warn_threshold_ms: u64,
201 pub filesystem_direct_io: bool,
203 pub category: Option<String>,
207 pub tags: Vec<String>,
211}
212
213impl Default for TorrentConfig {
214 fn default() -> Self {
215 Self {
216 listen_port: 6881,
217 max_peers: 128,
218 target_request_queue: 5,
219 download_dir: PathBuf::from("."),
220 enable_dht: true,
221 enable_pex: true,
222 enable_fast: false,
223 seed_ratio_limit: None,
224 seed_time_limit_secs: None,
225 inactive_seed_time_limit_secs: None,
226 strict_end_game: true,
227 upload_rate_limit: 0,
228 download_rate_limit: 0,
229 max_uploads_per_torrent: -1,
230 encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
231 enable_utp: true,
232 enable_web_seed: true,
233 enable_holepunch: true,
234 enable_bep40_eviction: true,
235 max_web_seeds: 4,
236 web_seed_retry_base_secs: 10,
237 web_seed_retry_factor: 6,
238 web_seed_retry_cap_secs: 3600,
239 web_seed_max_failures: 10,
240 super_seeding: false,
241 upload_only_announce: true,
242 hashing_threads: {
243 let cores = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
244 (cores / 4).clamp(2, 8)
245 },
246 sequential_download: false,
247 initial_picker_threshold: 4,
248 whole_pieces_threshold: 20,
249 snub_timeout_secs: 15,
250 readahead_pieces: 8,
251 streaming_timeout_escalation: true,
252 max_concurrent_stream_reads: 8,
253 proxy: crate::proxy::ProxyConfig::default(),
254 anonymous_mode: false,
255 share_mode: false,
256 enable_i2p: false,
257 allow_i2p_mixed: false,
258 ssl_listen_port: 0,
259 seed_choking_algorithm: SeedChokingAlgorithm::FastestUpload,
260 choking_algorithm: ChokingAlgorithm::FixedSlots,
261 piece_extent_affinity: true,
262 suggest_mode: false,
263 max_suggest_pieces: 10,
264 predictive_piece_announce_ms: 0,
265 mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
266 auto_sequential: true,
267 storage_mode: irontide_core::StorageMode::Auto,
268 preallocate_mode: None,
269 block_request_timeout_secs: 60,
270 enable_lsd: true,
271 force_proxy: false,
272 steal_threshold_ratio: 10.0,
273 steal_threshold_endgame: 3.0,
274 peer_read_timeout_secs: 10,
275 peer_write_timeout_secs: 10,
276 data_contribution_timeout_secs: 0,
277 pass0_grace_secs: 60,
279 proactive_evictions_per_minute_limit: 30,
280 eviction_ban_duration_secs: 600,
281 eviction_ban_set_cap: 1024,
282 choke_rotation_max_evictions: 0,
283 max_concurrent_connects: 128,
284 connect_soft_timeout: 3,
285 dispatch_backlog_cap: 8,
286 event_backlog_cap: 32,
287 use_actor_dispatch: true,
288 web_seed_progress_throttle_ms: 250,
289 url_security: crate::url_guard::UrlSecurityConfig::default(),
290 peer_connect_timeout: 10,
291 peer_dscp: 0x08,
292 initial_queue_depth: 128,
293 max_request_queue_depth: 250,
294 request_queue_time: 3.0,
295 max_metadata_size: 4 * 1024 * 1024,
296 max_message_size: 16 * 1024 * 1024,
297 max_piece_length: 32 * 1024 * 1024,
298 max_outstanding_requests: 500,
299 max_in_flight_pieces: 512,
300 use_block_stealing: true,
301 steal_stale_piece_secs: 2,
302 fixed_pipeline_depth: 128,
303 lock_warn_threshold_ms: 50,
304 filesystem_direct_io: false,
305 category: None,
306 tags: Vec::new(),
307 }
308 }
309}
310
311impl From<&crate::settings::Settings> for TorrentConfig {
312 fn from(s: &crate::settings::Settings) -> Self {
313 Self {
314 listen_port: 0, max_peers: s.max_peers_per_torrent,
316 target_request_queue: 5,
317 download_dir: s.download_dir.clone(),
318 enable_dht: s.enable_dht,
319 enable_pex: s.enable_pex,
320 enable_fast: s.enable_fast_extension,
321 seed_ratio_limit: s.seed_ratio_limit,
322 seed_time_limit_secs: s.seed_time_limit_secs,
323 inactive_seed_time_limit_secs: s.inactive_seed_time_limit_secs,
324 strict_end_game: s.strict_end_game,
325 upload_rate_limit: s.upload_rate_limit,
326 download_rate_limit: s.download_rate_limit,
327 max_uploads_per_torrent: s.max_uploads_per_torrent,
328 encryption_mode: s.encryption_mode,
329 enable_utp: s.enable_utp,
330 enable_web_seed: s.enable_web_seed,
331 enable_holepunch: s.enable_holepunch,
332 enable_bep40_eviction: s.enable_bep40_eviction,
333 max_web_seeds: s.max_web_seeds,
334 web_seed_retry_base_secs: s.web_seed_retry_base_secs,
335 web_seed_retry_factor: s.web_seed_retry_factor,
336 web_seed_retry_cap_secs: s.web_seed_retry_cap_secs,
337 web_seed_max_failures: s.web_seed_max_failures,
338 super_seeding: s.default_super_seeding,
339 upload_only_announce: s.upload_only_announce,
340 hashing_threads: s.hashing_threads,
341 sequential_download: false,
342 initial_picker_threshold: s.initial_picker_threshold,
343 whole_pieces_threshold: s.whole_pieces_threshold,
344 snub_timeout_secs: s.snub_timeout_secs,
345 readahead_pieces: s.readahead_pieces,
346 streaming_timeout_escalation: s.streaming_timeout_escalation,
347 max_concurrent_stream_reads: s.max_concurrent_stream_reads,
348 proxy: s.proxy.clone(),
349 anonymous_mode: s.anonymous_mode,
350 share_mode: s.default_share_mode,
351 enable_i2p: s.enable_i2p,
352 allow_i2p_mixed: s.allow_i2p_mixed,
353 ssl_listen_port: s.ssl_listen_port,
354 seed_choking_algorithm: s.seed_choking_algorithm,
355 choking_algorithm: s.choking_algorithm,
356 piece_extent_affinity: s.piece_extent_affinity,
357 suggest_mode: s.suggest_mode,
358 max_suggest_pieces: s.max_suggest_pieces,
359 predictive_piece_announce_ms: s.predictive_piece_announce_ms,
360 mixed_mode_algorithm: s.mixed_mode_algorithm,
361 auto_sequential: s.auto_sequential,
362 storage_mode: s.storage_mode,
363 preallocate_mode: s.preallocate_mode,
364 block_request_timeout_secs: s.block_request_timeout_secs,
365 enable_lsd: s.enable_lsd,
366 force_proxy: s.force_proxy,
367 steal_threshold_ratio: s.steal_threshold_ratio,
368 steal_threshold_endgame: s.steal_threshold_endgame,
369 peer_read_timeout_secs: s.peer_read_timeout_secs,
370 peer_write_timeout_secs: s.peer_write_timeout_secs,
371 data_contribution_timeout_secs: s.data_contribution_timeout_secs,
372 pass0_grace_secs: s.pass0_grace_secs,
373 proactive_evictions_per_minute_limit: s.proactive_evictions_per_minute_limit,
374 eviction_ban_duration_secs: s.eviction_ban_duration_secs,
375 eviction_ban_set_cap: s.eviction_ban_set_cap,
376 choke_rotation_max_evictions: s.choke_rotation_max_evictions,
377 max_concurrent_connects: s.max_concurrent_connects,
378 connect_soft_timeout: s.connect_soft_timeout,
379 dispatch_backlog_cap: s.dispatch_backlog_cap,
380 event_backlog_cap: s.event_backlog_cap,
381 use_actor_dispatch: s.use_actor_dispatch,
382 web_seed_progress_throttle_ms: s.web_seed_progress_throttle_ms,
383 url_security: crate::url_guard::UrlSecurityConfig::from(s),
384 peer_connect_timeout: s.peer_connect_timeout,
385 peer_dscp: s.peer_dscp,
386 initial_queue_depth: s.initial_queue_depth,
387 max_request_queue_depth: s.max_request_queue_depth,
388 request_queue_time: s.request_queue_time,
389 max_metadata_size: s.max_metadata_size,
390 max_message_size: s.max_message_size,
391 max_piece_length: s.max_piece_length,
392 max_outstanding_requests: s.max_outstanding_requests,
393 max_in_flight_pieces: s.max_in_flight_pieces,
394 use_block_stealing: s.use_block_stealing,
395 steal_stale_piece_secs: s.steal_stale_piece_secs,
396 fixed_pipeline_depth: s.fixed_pipeline_depth,
397 lock_warn_threshold_ms: s.lock_warn_threshold_ms,
398 filesystem_direct_io: s.filesystem_direct_io,
399 category: None,
403 tags: Vec::new(),
406 }
407 }
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
412pub enum TorrentState {
413 FetchingMetadata,
415 Checking,
417 Downloading,
419 Complete,
421 Seeding,
423 Paused,
425 Queued,
427 Stopped,
429 Sharing,
431}
432
433#[derive(Debug, Clone, Serialize)]
435pub struct TorrentStats {
436 pub state: TorrentState,
439 pub downloaded: u64,
441 pub uploaded: u64,
443 pub pieces_have: u32,
445 pub pieces_total: u32,
447 pub peers_connected: usize,
449 pub peers_available: usize,
451 pub checking_progress: f32,
453 pub peers_by_source: HashMap<crate::peer_state::PeerSource, usize>,
455
456 pub info_hashes: irontide_core::InfoHashes,
459 pub name: String,
461
462 pub has_metadata: bool,
465 pub is_seeding: bool,
467 pub is_finished: bool,
469 pub is_paused: bool,
471 pub is_queued: bool,
473 pub auto_managed: bool,
475 pub sequential_download: bool,
477 pub super_seeding: bool,
479 #[serde(default)]
485 pub user_seed_mode: bool,
486 #[serde(default)]
488 pub user_forced: bool,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub seed_ratio_override: Option<f64>,
492 pub has_incoming: bool,
494 pub need_save_resume: bool,
496 pub moving_storage: bool,
498
499 pub progress: f32,
502 pub progress_ppm: u32,
504 pub total_done: u64,
506 pub total: u64,
508 pub total_wanted_done: u64,
510 pub total_wanted: u64,
512 pub block_size: u32,
514
515 pub total_download: u64,
518 pub total_upload: u64,
520 pub total_payload_download: u64,
522 pub total_payload_upload: u64,
524 pub total_failed_bytes: u64,
526 pub total_redundant_bytes: u64,
528
529 pub all_time_download: u64,
532 pub all_time_upload: u64,
534
535 pub download_rate: u64,
538 pub upload_rate: u64,
540 pub download_payload_rate: u64,
542 pub upload_payload_rate: u64,
544
545 pub num_peers: usize,
548 pub num_seeds: usize,
550 pub num_complete: i32,
552 pub num_incomplete: i32,
554 pub list_seeds: usize,
556 pub list_peers: usize,
558 pub connect_candidates: usize,
560 pub num_connections: usize,
562 pub num_uploads: usize,
564 pub unique_peers_attempted: u64,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub pipeline: Option<crate::peer_states::PeerPipelineSnapshot>,
569 pub choke_rotations: u64,
571 pub piece_steals: u64,
573 #[serde(default)]
575 pub holepunch_relayed: u64,
576 #[serde(default)]
578 pub dispatch_pieces_queued: u32,
579 #[serde(default)]
581 pub dispatch_pieces_inflight: u32,
582
583 pub connections_limit: usize,
586 pub uploads_limit: usize,
588
589 pub distributed_full_copies: u32,
592 pub distributed_fraction: u32,
594 pub distributed_copies: f32,
596
597 pub current_tracker: String,
600 pub announcing_to_trackers: bool,
602 pub announcing_to_lsd: bool,
604 pub announcing_to_dht: bool,
606
607 pub added_time: i64,
610 pub completed_time: i64,
612 pub last_seen_complete: i64,
614 pub last_upload: i64,
616 pub last_download: i64,
618
619 pub active_duration: i64,
622 pub finished_duration: i64,
624 pub seeding_duration: i64,
626
627 pub save_path: String,
630
631 pub queue_position: i32,
634
635 pub error: String,
638 pub error_file: i32,
640
641 #[serde(default)]
644 pub category: Option<String>,
645 #[serde(default)]
648 pub created_by: Option<String>,
649 #[serde(default)]
652 pub creation_date: Option<i64>,
653 #[serde(default)]
656 pub piece_size: u64,
657
658 #[serde(default)]
661 pub tags: Vec<String>,
662}
663
664impl Default for TorrentStats {
665 fn default() -> Self {
666 Self {
667 state: TorrentState::Paused,
669 downloaded: 0,
670 uploaded: 0,
671 pieces_have: 0,
672 pieces_total: 0,
673 peers_connected: 0,
674 peers_available: 0,
675 checking_progress: 0.0,
676 peers_by_source: HashMap::new(),
677
678 info_hashes: irontide_core::InfoHashes::v1_only(irontide_core::Id20::from([0u8; 20])),
680 name: String::new(),
681
682 has_metadata: false,
684 is_seeding: false,
685 is_finished: false,
686 is_paused: false,
687 is_queued: false,
688 auto_managed: false,
689 sequential_download: false,
690 super_seeding: false,
691 user_seed_mode: false,
692 user_forced: false,
693 seed_ratio_override: None,
694 has_incoming: false,
695 need_save_resume: false,
696 moving_storage: false,
697
698 progress: 0.0,
700 progress_ppm: 0,
701 total_done: 0,
702 total: 0,
703 total_wanted_done: 0,
704 total_wanted: 0,
705 block_size: 16384,
706
707 total_download: 0,
709 total_upload: 0,
710 total_payload_download: 0,
711 total_payload_upload: 0,
712 total_failed_bytes: 0,
713 total_redundant_bytes: 0,
714
715 all_time_download: 0,
717 all_time_upload: 0,
718
719 download_rate: 0,
721 upload_rate: 0,
722 download_payload_rate: 0,
723 upload_payload_rate: 0,
724
725 num_peers: 0,
727 num_seeds: 0,
728 num_complete: -1,
729 num_incomplete: -1,
730 list_seeds: 0,
731 list_peers: 0,
732 connect_candidates: 0,
733 num_connections: 0,
734 num_uploads: 0,
735 unique_peers_attempted: 0,
736 pipeline: None,
737 choke_rotations: 0,
738 piece_steals: 0,
739 holepunch_relayed: 0,
740 dispatch_pieces_queued: 0,
741 dispatch_pieces_inflight: 0,
742
743 connections_limit: 0,
745 uploads_limit: 0,
746
747 distributed_full_copies: 0,
749 distributed_fraction: 0,
750 distributed_copies: 0.0,
751
752 current_tracker: String::new(),
754 announcing_to_trackers: false,
755 announcing_to_lsd: false,
756 announcing_to_dht: false,
757
758 added_time: 0,
760 completed_time: 0,
761 last_seen_complete: 0,
762 last_upload: 0,
763 last_download: 0,
764
765 active_duration: 0,
767 finished_duration: 0,
768 seeding_duration: 0,
769
770 save_path: String::new(),
772
773 queue_position: -1,
775
776 error: String::new(),
778 error_file: -1,
779
780 category: None,
782 created_by: None,
783 creation_date: None,
784 piece_size: 0,
785
786 tags: Vec::new(),
788 }
789 }
790}
791
792#[derive(Debug, Clone, Serialize)]
797pub struct TorrentSummary {
798 pub info_hash: String,
800 pub name: String,
802 pub state: TorrentState,
804 pub progress: f64,
806 pub download_rate: u64,
808 pub upload_rate: u64,
810 pub total_size: u64,
812 pub num_peers: usize,
814 pub num_seeds: usize,
816 pub all_time_upload: u64,
818 pub all_time_download: u64,
820 pub added_time: i64,
822 pub user_seed_mode: bool,
824 pub super_seeding: bool,
828 pub user_forced: bool,
830 pub checking_progress: f32,
832}
833
834impl From<&TorrentStats> for TorrentSummary {
835 fn from(s: &TorrentStats) -> Self {
836 Self {
837 info_hash: s.info_hashes.v1.map(|h| h.to_hex()).unwrap_or_default(),
838 name: s.name.clone(),
839 state: s.state,
840 progress: f64::from(s.progress),
841 download_rate: s.download_rate,
842 upload_rate: s.upload_rate,
843 total_size: s.total,
844 num_peers: s.num_peers,
845 num_seeds: s.num_seeds,
846 all_time_upload: s.all_time_upload,
847 all_time_download: s.all_time_download,
848 added_time: s.added_time,
849 user_seed_mode: s.user_seed_mode,
850 super_seeding: s.super_seeding,
851 user_forced: s.user_forced,
852 checking_progress: s.checking_progress,
853 }
854 }
855}
856
857#[derive(Debug, Clone)]
860pub(crate) struct BlockEntry {
861 pub index: u32, pub begin: u32, pub length: u32, }
865
866#[derive(Debug)]
868#[allow(dead_code)] pub(crate) enum PeerEvent {
870 Bitfield {
871 peer_addr: SocketAddr,
872 bitfield: Bitfield,
873 },
874 Have {
875 peer_addr: SocketAddr,
876 index: u32,
877 },
878 DontHave {
880 peer_addr: SocketAddr,
881 index: u32,
882 },
883 PieceData {
884 peer_addr: SocketAddr,
885 index: u32,
886 begin: u32,
887 data: Bytes,
888 },
889 PieceBlocksBatch {
892 peer_addr: SocketAddr,
893 blocks: Vec<BlockEntry>,
894 },
895 PeerChoking {
896 peer_addr: SocketAddr,
897 choking: bool,
898 },
899 PeerInterested {
900 peer_addr: SocketAddr,
901 interested: bool,
902 },
903 ExtHandshake {
904 peer_addr: SocketAddr,
905 handshake: ExtHandshake,
906 },
907 MetadataPiece {
908 peer_addr: SocketAddr,
909 piece: u32,
910 data: Bytes,
911 total_size: u64,
912 },
913 MetadataReject {
914 peer_addr: SocketAddr,
915 piece: u32,
916 },
917 PexPeers {
918 new_peers: Vec<SocketAddr>,
919 },
920 TrackersReceived {
921 tracker_urls: Vec<String>,
922 },
923 IncomingRequest {
924 peer_addr: SocketAddr,
925 index: u32,
926 begin: u32,
927 length: u32,
928 },
929 RejectRequest {
930 peer_addr: SocketAddr,
931 index: u32,
932 begin: u32,
933 length: u32,
934 },
935 AllowedFast {
936 peer_addr: SocketAddr,
937 index: u32,
938 },
939 SuggestPiece {
940 peer_addr: SocketAddr,
941 index: u32,
942 },
943 TransportIdentified {
945 peer_addr: SocketAddr,
946 transport: crate::rate_limiter::PeerTransport,
947 },
948 HandshakeComplete {
952 peer_addr: SocketAddr,
953 is_encrypted: bool,
955 },
956 Disconnected {
957 peer_addr: SocketAddr,
958 reason: Option<String>,
959 },
960 WebSeedPieceData {
961 url: String,
962 index: u32,
963 data: Bytes,
964 },
965 WebSeedError {
966 url: String,
967 piece: u32,
968 message: String,
969 },
970 WebSeedProgress {
978 url: String,
979 bytes: u64,
980 rate_bps: u64,
981 error: Option<String>,
982 },
983 WebSeedRetryReady {
985 url: String,
986 },
987 WebSeedPermanentFailure {
989 url: String,
990 },
991 HashesReceived {
993 peer_addr: SocketAddr,
994 request: irontide_core::HashRequest,
995 hashes: Vec<irontide_core::Id32>,
996 },
997 HashRequestRejected {
999 peer_addr: SocketAddr,
1000 request: irontide_core::HashRequest,
1001 },
1002 IncomingHashRequest {
1004 peer_addr: SocketAddr,
1005 request: irontide_core::HashRequest,
1006 },
1007 HolepunchRendezvous {
1009 peer_addr: SocketAddr,
1010 target: SocketAddr,
1011 },
1012 HolepunchConnect {
1014 peer_addr: SocketAddr,
1015 target: SocketAddr,
1016 },
1017 HolepunchError {
1019 peer_addr: SocketAddr,
1020 target: SocketAddr,
1021 error_code: u32,
1022 },
1023 MseRetry {
1027 peer_addr: SocketAddr,
1028 cmd_tx: tokio::sync::mpsc::Sender<PeerCommand>,
1029 },
1030 PieceReleased {
1032 peer_addr: SocketAddr,
1033 piece: u32,
1034 },
1035 AcquirePiece {
1039 peer_addr: SocketAddr,
1040 response_tx: tokio::sync::oneshot::Sender<crate::piece_reservation::AcquireResponse>,
1041 },
1042}
1043
1044#[derive(Debug)]
1046#[allow(dead_code)] pub(crate) enum PeerCommand {
1048 Request {
1049 index: u32,
1050 begin: u32,
1051 length: u32,
1052 },
1053 Cancel {
1054 index: u32,
1055 begin: u32,
1056 length: u32,
1057 },
1058 SetChoking(bool),
1059 SetInterested(bool),
1060 Have(u32),
1061 RequestMetadata {
1062 piece: u32,
1063 },
1064 RejectRequest {
1065 index: u32,
1066 begin: u32,
1067 length: u32,
1068 },
1069 AllowedFast(u32),
1070 SendPiece {
1071 index: u32,
1072 begin: u32,
1073 data: Bytes,
1074 },
1075 SendExtHandshake(irontide_wire::ExtHandshake),
1077 SuggestPiece(u32),
1079 SendHashRequest(irontide_core::HashRequest),
1081 SendHashes {
1083 request: irontide_core::HashRequest,
1084 hashes: Vec<irontide_core::Id32>,
1085 },
1086 SendHashReject(irontide_core::HashRequest),
1088 SendPex {
1090 message: crate::pex::PexMessage,
1091 },
1092 SendHolepunch(irontide_wire::HolepunchMessage),
1094 UpdateNumPieces(u32),
1096 StopRequesting,
1102 StartRequesting {
1105 piece_notify: std::sync::Arc<tokio::sync::Notify>,
1106 disk_handle: Option<crate::disk::DiskHandle>,
1107 write_error_tx: tokio::sync::mpsc::Sender<crate::disk::DiskWriteError>,
1108 lengths: irontide_core::Lengths,
1109 },
1110 Shutdown,
1111}
1112
1113pub(crate) trait AsyncReadWrite:
1118 tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send
1119{
1120}
1121
1122impl<T> AsyncReadWrite for T where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send {}
1123
1124pub(crate) struct BoxedAsyncStream(pub Box<dyn AsyncReadWrite>);
1128
1129impl std::fmt::Debug for BoxedAsyncStream {
1130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1131 f.write_str("BoxedAsyncStream(..)")
1132 }
1133}
1134
1135#[derive(Debug)]
1137#[allow(dead_code)] pub(crate) enum TorrentCommand {
1139 AddPeers {
1140 peers: Vec<SocketAddr>,
1141 source: crate::peer_state::PeerSource,
1142 },
1143 Stats {
1144 reply: oneshot::Sender<TorrentStats>,
1145 },
1146 Pause,
1147 Queue,
1148 Resume,
1149 ForceResume,
1151 Shutdown,
1152 SetCategory {
1155 category: Option<String>,
1156 reply: oneshot::Sender<()>,
1157 },
1158 SetTags {
1163 tags: Vec<String>,
1164 reply: oneshot::Sender<()>,
1165 },
1166 GetWebSeeds {
1171 reply: oneshot::Sender<Vec<String>>,
1172 },
1173 GetPieceStates {
1179 reply: oneshot::Sender<Vec<u8>>,
1180 },
1181 GetPieceHashes {
1188 offset: u32,
1189 limit: u32,
1190 reply: oneshot::Sender<Vec<String>>,
1191 },
1192 SaveResumeData {
1193 reply: oneshot::Sender<crate::Result<irontide_core::FastResumeData>>,
1194 },
1195 SetFilePriority {
1196 index: usize,
1197 priority: irontide_core::FilePriority,
1198 reply: oneshot::Sender<crate::Result<()>>,
1199 },
1200 FilePriorities {
1201 reply: oneshot::Sender<Vec<irontide_core::FilePriority>>,
1202 },
1203 ForceReannounce,
1204 TrackerList {
1205 reply: oneshot::Sender<Vec<crate::tracker_manager::TrackerInfo>>,
1206 },
1207 Scrape {
1208 reply: oneshot::Sender<Option<(String, irontide_tracker::ScrapeInfo)>>,
1209 },
1210 IncomingPeer {
1212 stream: crate::transport::BoxedStream,
1213 addr: SocketAddr,
1214 },
1215 OpenFile {
1217 file_index: usize,
1218 reply: oneshot::Sender<crate::Result<crate::streaming::FileStreamHandle>>,
1219 },
1220 UpdateExternalIp {
1222 ip: std::net::IpAddr,
1223 },
1224 MoveStorage {
1226 new_path: PathBuf,
1227 reply: oneshot::Sender<crate::Result<()>>,
1228 },
1229 SpawnSslPeer {
1233 addr: SocketAddr,
1234 stream: BoxedAsyncStream,
1235 },
1236 SetDownloadLimit {
1238 bytes_per_sec: u64,
1239 reply: oneshot::Sender<()>,
1240 },
1241 SetUploadLimit {
1243 bytes_per_sec: u64,
1244 reply: oneshot::Sender<()>,
1245 },
1246 DownloadLimit {
1248 reply: oneshot::Sender<u64>,
1249 },
1250 UploadLimit {
1252 reply: oneshot::Sender<u64>,
1253 },
1254 SetSequentialDownload {
1256 enabled: bool,
1257 reply: oneshot::Sender<()>,
1258 },
1259 IsSequentialDownload {
1261 reply: oneshot::Sender<bool>,
1262 },
1263 SetSuperSeeding {
1265 enabled: bool,
1266 reply: oneshot::Sender<()>,
1267 },
1268 IsSuperSeeding {
1270 reply: oneshot::Sender<bool>,
1271 },
1272 SetSeedMode {
1278 enabled: bool,
1279 reply: oneshot::Sender<()>,
1280 },
1281 SetSeedRatioLimit {
1283 limit: Option<f64>,
1284 reply: oneshot::Sender<()>,
1285 },
1286 AddTracker {
1288 url: String,
1289 },
1290 ReplaceTrackers {
1292 urls: Vec<String>,
1293 reply: oneshot::Sender<()>,
1294 },
1295 ForceRecheck {
1297 reply: oneshot::Sender<crate::Result<()>>,
1298 },
1299 RenameFile {
1301 file_index: usize,
1302 new_name: String,
1303 reply: oneshot::Sender<crate::Result<()>>,
1304 },
1305 SetMaxConnections {
1307 limit: usize,
1308 reply: oneshot::Sender<()>,
1309 },
1310 MaxConnections {
1312 reply: oneshot::Sender<usize>,
1313 },
1314 SetMaxUploads {
1316 limit: usize,
1317 reply: oneshot::Sender<()>,
1318 },
1319 MaxUploads {
1321 reply: oneshot::Sender<usize>,
1322 },
1323 GetPeerInfo {
1325 reply: oneshot::Sender<Vec<PeerInfo>>,
1326 },
1327 GetDownloadQueue {
1329 reply: oneshot::Sender<Vec<PartialPieceInfo>>,
1330 },
1331 HavePiece {
1333 index: u32,
1334 reply: oneshot::Sender<bool>,
1335 },
1336 PieceAvailability {
1338 reply: oneshot::Sender<Vec<u32>>,
1339 },
1340 FileProgress {
1342 reply: oneshot::Sender<Vec<u64>>,
1343 },
1344 InfoHashes {
1346 reply: oneshot::Sender<irontide_core::InfoHashes>,
1347 },
1348 TorrentFile {
1350 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
1351 },
1352 TorrentFileV2 {
1354 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV2>>,
1355 },
1356 ForceDhtAnnounce,
1358 ReadPiece {
1360 index: u32,
1361 reply: oneshot::Sender<crate::Result<bytes::Bytes>>,
1362 },
1363 FlushCache {
1365 reply: oneshot::Sender<crate::Result<()>>,
1366 },
1367 ClearError,
1369 FileStatus {
1371 reply: oneshot::Sender<Vec<crate::types::FileStatus>>,
1372 },
1373 Flags {
1375 reply: oneshot::Sender<TorrentFlags>,
1376 },
1377 SetFlags {
1379 flags: TorrentFlags,
1380 reply: oneshot::Sender<()>,
1381 },
1382 UnsetFlags {
1384 flags: TorrentFlags,
1385 reply: oneshot::Sender<()>,
1386 },
1387 ConnectPeer {
1389 addr: SocketAddr,
1390 },
1391 ClearSaveResumeFlag,
1393 RestoreResumeBitmap {
1398 pieces: Vec<u8>,
1400 reply: oneshot::Sender<crate::Result<()>>,
1402 },
1403 RestoreWebSeedStats {
1409 stats: HashMap<String, irontide_core::WebSeedStats>,
1411 reply: oneshot::Sender<crate::Result<()>>,
1413 },
1414 GetPeerSourceCounts {
1417 reply: oneshot::Sender<(usize, usize)>,
1419 },
1420 QueryUnchokeDurations {
1426 reply: oneshot::Sender<HashMap<SocketAddr, std::time::Duration>>,
1428 },
1429 GetWebSeedStats {
1432 reply: oneshot::Sender<Vec<irontide_core::WebSeedStats>>,
1434 },
1435 PreResolvedMetadata {
1442 info_bytes: Vec<u8>,
1444 peers: Vec<SocketAddr>,
1447 },
1448 GetMeta {
1456 reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
1459 },
1460 #[cfg(feature = "test-util")]
1467 TestInjectMetadata {
1468 info_bytes: Vec<u8>,
1470 reply: oneshot::Sender<()>,
1472 },
1473 UpdateSettings(SettingsDelta),
1479}
1480
1481#[derive(Debug, Clone, Default)]
1485#[allow(
1486 clippy::option_option,
1487 reason = "outer Option = unchanged, inner Option = the actual Settings value (None = unlimited)"
1488)]
1489pub struct SettingsDelta {
1490 pub enable_dht: Option<bool>,
1491 pub enable_pex: Option<bool>,
1492 pub max_peers: Option<usize>,
1493 pub seed_ratio_limit: Option<Option<f64>>,
1494 pub seed_time_limit_secs: Option<Option<u64>>,
1495 pub inactive_seed_time_limit_secs: Option<Option<u64>>,
1496 pub max_ratio_action: Option<crate::MaxRatioAction>,
1497 pub encryption_mode: Option<irontide_wire::mse::EncryptionMode>,
1498 pub anonymous_mode: Option<bool>,
1499 pub max_uploads_per_torrent: Option<i32>,
1502 pub save_resume_interval_secs: Option<u64>,
1504 pub hashing_threads: Option<usize>,
1506 pub ip_filter_enabled: Option<bool>,
1508 pub notify_on_complete: Option<bool>,
1511 pub notify_on_error: Option<bool>,
1513 pub on_complete_program: Option<Option<std::path::PathBuf>>,
1516 pub use_incomplete_dir: Option<bool>,
1518 pub incomplete_dir: Option<Option<std::path::PathBuf>>,
1520 pub default_skip_hash_check: Option<bool>,
1522 pub incomplete_extension_enabled: Option<bool>,
1524 pub watched_folder: Option<Option<std::path::PathBuf>>,
1526 pub delete_torrent_after_add: Option<bool>,
1528 pub move_completed_enabled: Option<bool>,
1530 pub move_completed_to: Option<Option<std::path::PathBuf>>,
1532 pub ip_filter_auto_refresh: Option<bool>,
1535 pub web_ui_https_enabled: Option<bool>,
1537 pub network_interface: Option<Option<String>>,
1539 pub default_add_paused: Option<bool>,
1541}
1542
1543impl SettingsDelta {
1544 #[must_use]
1545 pub fn from_diff(old: &crate::settings::Settings, new: &crate::settings::Settings) -> Self {
1546 let mut d = Self::default();
1547 if old.enable_dht != new.enable_dht {
1548 d.enable_dht = Some(new.enable_dht);
1549 }
1550 if old.enable_pex != new.enable_pex {
1551 d.enable_pex = Some(new.enable_pex);
1552 }
1553 if old.max_peers_per_torrent != new.max_peers_per_torrent {
1554 d.max_peers = Some(new.max_peers_per_torrent);
1555 }
1556 if old.seed_ratio_limit != new.seed_ratio_limit {
1557 d.seed_ratio_limit = Some(new.seed_ratio_limit);
1558 }
1559 if old.seed_time_limit_secs != new.seed_time_limit_secs {
1560 d.seed_time_limit_secs = Some(new.seed_time_limit_secs);
1561 }
1562 if old.inactive_seed_time_limit_secs != new.inactive_seed_time_limit_secs {
1563 d.inactive_seed_time_limit_secs = Some(new.inactive_seed_time_limit_secs);
1564 }
1565 if old.max_ratio_action != new.max_ratio_action {
1566 d.max_ratio_action = Some(new.max_ratio_action);
1567 }
1568 if old.encryption_mode != new.encryption_mode {
1569 d.encryption_mode = Some(new.encryption_mode);
1570 }
1571 if old.anonymous_mode != new.anonymous_mode {
1572 d.anonymous_mode = Some(new.anonymous_mode);
1573 }
1574 if old.max_uploads_per_torrent != new.max_uploads_per_torrent {
1575 d.max_uploads_per_torrent = Some(new.max_uploads_per_torrent);
1576 }
1577 if old.save_resume_interval_secs != new.save_resume_interval_secs {
1578 d.save_resume_interval_secs = Some(new.save_resume_interval_secs);
1579 }
1580 if old.hashing_threads != new.hashing_threads {
1581 d.hashing_threads = Some(new.hashing_threads);
1582 }
1583 if old.ip_filter_enabled != new.ip_filter_enabled {
1584 d.ip_filter_enabled = Some(new.ip_filter_enabled);
1585 }
1586 if old.notify_on_complete != new.notify_on_complete {
1588 d.notify_on_complete = Some(new.notify_on_complete);
1589 }
1590 if old.notify_on_error != new.notify_on_error {
1591 d.notify_on_error = Some(new.notify_on_error);
1592 }
1593 if old.on_complete_program != new.on_complete_program {
1594 d.on_complete_program = Some(new.on_complete_program.clone());
1595 }
1596 if old.use_incomplete_dir != new.use_incomplete_dir {
1597 d.use_incomplete_dir = Some(new.use_incomplete_dir);
1598 }
1599 if old.incomplete_dir != new.incomplete_dir {
1600 d.incomplete_dir = Some(new.incomplete_dir.clone());
1601 }
1602 if old.default_skip_hash_check != new.default_skip_hash_check {
1603 d.default_skip_hash_check = Some(new.default_skip_hash_check);
1604 }
1605 if old.incomplete_extension_enabled != new.incomplete_extension_enabled {
1606 d.incomplete_extension_enabled = Some(new.incomplete_extension_enabled);
1607 }
1608 if old.watched_folder != new.watched_folder {
1609 d.watched_folder = Some(new.watched_folder.clone());
1610 }
1611 if old.delete_torrent_after_add != new.delete_torrent_after_add {
1612 d.delete_torrent_after_add = Some(new.delete_torrent_after_add);
1613 }
1614 if old.move_completed_enabled != new.move_completed_enabled {
1615 d.move_completed_enabled = Some(new.move_completed_enabled);
1616 }
1617 if old.move_completed_to != new.move_completed_to {
1618 d.move_completed_to = Some(new.move_completed_to.clone());
1619 }
1620 if old.ip_filter_auto_refresh != new.ip_filter_auto_refresh {
1621 d.ip_filter_auto_refresh = Some(new.ip_filter_auto_refresh);
1622 }
1623 if old.web_ui_https_enabled != new.web_ui_https_enabled {
1624 d.web_ui_https_enabled = Some(new.web_ui_https_enabled);
1625 }
1626 if old.network_interface != new.network_interface {
1627 d.network_interface = Some(new.network_interface.clone());
1628 }
1629 if old.default_add_paused != new.default_add_paused {
1630 d.default_add_paused = Some(new.default_add_paused);
1631 }
1632 d
1633 }
1634
1635 #[must_use]
1636 pub fn is_empty(&self) -> bool {
1637 self.enable_dht.is_none()
1638 && self.enable_pex.is_none()
1639 && self.max_peers.is_none()
1640 && self.seed_ratio_limit.is_none()
1641 && self.seed_time_limit_secs.is_none()
1642 && self.inactive_seed_time_limit_secs.is_none()
1643 && self.max_ratio_action.is_none()
1644 && self.encryption_mode.is_none()
1645 && self.anonymous_mode.is_none()
1646 && self.max_uploads_per_torrent.is_none()
1647 && self.save_resume_interval_secs.is_none()
1648 && self.hashing_threads.is_none()
1649 && self.ip_filter_enabled.is_none()
1650 && self.notify_on_complete.is_none()
1652 && self.notify_on_error.is_none()
1653 && self.on_complete_program.is_none()
1654 && self.use_incomplete_dir.is_none()
1655 && self.incomplete_dir.is_none()
1656 && self.default_skip_hash_check.is_none()
1657 && self.incomplete_extension_enabled.is_none()
1658 && self.watched_folder.is_none()
1659 && self.delete_torrent_after_add.is_none()
1660 && self.move_completed_enabled.is_none()
1661 && self.move_completed_to.is_none()
1662 && self.ip_filter_auto_refresh.is_none()
1663 && self.web_ui_https_enabled.is_none()
1664 && self.network_interface.is_none()
1665 && self.default_add_paused.is_none()
1666 }
1667}
1668
1669
1670#[derive(Debug, Clone, Serialize, Deserialize)]
1672pub struct PeerInfo {
1673 pub addr: SocketAddr,
1675 pub client: String,
1677 pub peer_choking: bool,
1679 pub peer_interested: bool,
1681 pub am_choking: bool,
1683 pub am_interested: bool,
1685 pub download_rate: u64,
1687 pub upload_rate: u64,
1689 pub num_pieces: u32,
1691 pub source: crate::peer_state::PeerSource,
1693 pub supports_fast: bool,
1695 pub upload_only: bool,
1697 pub snubbed: bool,
1699 pub connected_duration_secs: u64,
1701 pub num_pending_requests: usize,
1703 pub num_incoming_requests: usize,
1705 #[serde(default)]
1709 pub is_optimistic: bool,
1710 #[serde(default)]
1714 pub is_encrypted: bool,
1715 #[serde(default)]
1718 pub uses_utp: bool,
1719 #[serde(default)]
1723 pub uses_holepunch: bool,
1724 #[serde(default)]
1726 pub in_flight_requests: u32,
1727 #[serde(default)]
1729 pub target_pipeline_depth: u32,
1730}
1731
1732#[derive(Debug, Clone, Serialize)]
1734pub struct PartialPieceInfo {
1735 pub piece_index: u32,
1737 pub blocks_in_piece: u32,
1739 pub blocks_assigned: u32,
1741}
1742
1743#[derive(Debug, Clone, Serialize)]
1745pub struct FileInfo {
1746 pub path: PathBuf,
1748 pub length: u64,
1750}
1751
1752#[derive(Debug, Clone, Serialize)]
1754pub struct TorrentInfo {
1755 pub info_hash: irontide_core::Id20,
1757 pub name: String,
1759 pub total_length: u64,
1761 pub piece_length: u64,
1763 pub num_pieces: u32,
1765 pub files: Vec<FileInfo>,
1767 pub private: bool,
1769}
1770
1771#[derive(Debug, Clone, Serialize, Deserialize)]
1773pub struct SessionStats {
1774 pub active_torrents: usize,
1776 pub total_downloaded: u64,
1778 pub total_uploaded: u64,
1780 pub dht_nodes: usize,
1782}
1783
1784#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1786pub enum FileMode {
1787 ReadOnly,
1789 ReadWrite,
1791 Closed,
1793}
1794
1795#[derive(Debug, Clone, Serialize)]
1797pub struct FileStatus {
1798 pub open: bool,
1800 pub mode: FileMode,
1802}
1803
1804bitflags! {
1805 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1810 pub struct TorrentFlags: u32 {
1811 const PAUSED = 0x1;
1813 const AUTO_MANAGED = 0x2;
1815 const SEQUENTIAL_DOWNLOAD = 0x4;
1817 const SUPER_SEEDING = 0x8;
1819 const UPLOAD_ONLY = 0x10;
1821 }
1822}
1823
1824#[derive(Debug, Clone, Serialize)]
1828pub struct DebugState {
1829 pub torrents: Vec<DebugTorrentState>,
1831}
1832
1833#[derive(Debug, Clone, Serialize)]
1835pub struct DebugTorrentState {
1836 pub info_hash: String,
1838 pub state: String,
1840 #[serde(default)]
1842 pub num_peers: usize,
1843 #[serde(default)]
1845 pub dispatch: DebugDispatchState,
1846 #[serde(default)]
1848 pub peers: Vec<DebugPeerState>,
1849}
1850
1851#[derive(Debug, Clone, Default, Serialize)]
1853pub struct DebugDispatchState {
1854 #[serde(default)]
1856 pub acquire_total: i64,
1857 #[serde(default)]
1859 pub acquire_none_total: i64,
1860 #[serde(default)]
1862 pub acquire_us: i64,
1863 #[serde(default)]
1865 pub notify_wakeup_total: i64,
1866 #[serde(default)]
1868 pub pieces_queued: u32,
1869 #[serde(default)]
1871 pub pieces_inflight: u32,
1872}
1873
1874#[derive(Debug, Clone, Serialize)]
1876pub struct DebugPeerState {
1877 pub addr: SocketAddr,
1879 #[serde(default)]
1881 pub in_flight: u32,
1882 #[serde(default)]
1884 pub target_depth: u32,
1885 #[serde(default)]
1887 pub choking: bool,
1888 #[serde(default)]
1890 pub download_rate: u64,
1891}
1892
1893pub type StorageFactory = Box<
1895 dyn Fn(
1896 &irontide_core::TorrentMetaV1,
1897 &std::path::Path,
1898 ) -> std::sync::Arc<dyn irontide_storage::TorrentStorage>
1899 + Send
1900 + Sync,
1901>;
1902
1903#[cfg(test)]
1904mod tests {
1905 use super::*;
1906
1907 #[test]
1908 fn torrent_config_strict_end_game_default() {
1909 let config = TorrentConfig::default();
1910 assert!(config.strict_end_game);
1911 }
1912
1913 #[test]
1914 fn torrent_config_bandwidth_defaults() {
1915 let config = TorrentConfig::default();
1916 assert_eq!(config.upload_rate_limit, 0);
1917 assert_eq!(config.download_rate_limit, 0);
1918 }
1919
1920 #[test]
1921 fn torrent_config_encryption_default() {
1922 let cfg = TorrentConfig::default();
1923 assert_eq!(
1924 cfg.encryption_mode,
1925 irontide_wire::mse::EncryptionMode::Disabled
1926 );
1927 }
1928
1929 #[test]
1930 fn torrent_config_utp_default() {
1931 let cfg = TorrentConfig::default();
1932 assert!(cfg.enable_utp);
1933 }
1934
1935 #[test]
1936 fn torrent_config_web_seed_defaults() {
1937 let cfg = TorrentConfig::default();
1938 assert!(cfg.enable_web_seed);
1939 assert_eq!(cfg.max_web_seeds, 4);
1940 }
1941
1942 #[test]
1943 fn torrent_config_super_seeding_default() {
1944 let cfg = TorrentConfig::default();
1945 assert!(!cfg.super_seeding);
1946 assert!(cfg.upload_only_announce);
1947 }
1948
1949 #[test]
1950 fn torrent_config_picker_defaults() {
1951 let cfg = TorrentConfig::default();
1952 assert!(!cfg.sequential_download);
1953 assert_eq!(cfg.initial_picker_threshold, 4);
1954 assert_eq!(cfg.whole_pieces_threshold, 20);
1955 assert_eq!(cfg.snub_timeout_secs, 15);
1956 assert_eq!(cfg.readahead_pieces, 8);
1957 assert!(cfg.streaming_timeout_escalation);
1958 }
1959
1960 #[test]
1961 fn torrent_stats_has_peers_by_source() {
1962 use crate::peer_state::PeerSource;
1963 use std::collections::HashMap;
1964
1965 let stats = TorrentStats {
1966 state: TorrentState::Downloading,
1967 pieces_total: 10,
1968 ..Default::default()
1969 };
1970 assert!(stats.peers_by_source.is_empty());
1971
1972 let mut map = HashMap::new();
1973 map.insert(PeerSource::Tracker, 5);
1974 map.insert(PeerSource::Dht, 3);
1975 let stats2 = TorrentStats {
1976 peers_by_source: map.clone(),
1977 ..stats
1978 };
1979 assert_eq!(stats2.peers_by_source[&PeerSource::Tracker], 5);
1980 assert_eq!(stats2.peers_by_source[&PeerSource::Dht], 3);
1981 }
1982
1983 #[test]
1984 fn torrent_stats_default_values() {
1985 let stats = TorrentStats::default();
1986
1987 assert_eq!(stats.state, TorrentState::Paused);
1989
1990 assert_eq!(stats.downloaded, 0);
1992 assert_eq!(stats.uploaded, 0);
1993 assert_eq!(stats.pieces_have, 0);
1994 assert_eq!(stats.pieces_total, 0);
1995 assert_eq!(stats.peers_connected, 0);
1996 assert_eq!(stats.peers_available, 0);
1997 assert!((stats.checking_progress - 0.0).abs() < f32::EPSILON);
1998 assert!(stats.peers_by_source.is_empty());
1999
2000 assert_eq!(
2002 stats.info_hashes,
2003 irontide_core::InfoHashes::v1_only(irontide_core::Id20::from([0u8; 20]))
2004 );
2005 assert!(stats.name.is_empty());
2006
2007 assert!(!stats.has_metadata);
2009 assert!(!stats.is_seeding);
2010 assert!(!stats.is_finished);
2011 assert!(!stats.is_paused);
2012 assert!(!stats.auto_managed);
2013 assert!(!stats.sequential_download);
2014 assert!(!stats.super_seeding);
2015 assert!(!stats.has_incoming);
2016 assert!(!stats.need_save_resume);
2017 assert!(!stats.moving_storage);
2018
2019 assert!((stats.progress - 0.0).abs() < f32::EPSILON);
2021 assert_eq!(stats.progress_ppm, 0);
2022 assert_eq!(stats.total_done, 0);
2023 assert_eq!(stats.total, 0);
2024 assert_eq!(stats.total_wanted_done, 0);
2025 assert_eq!(stats.total_wanted, 0);
2026 assert_eq!(stats.block_size, 16384);
2027
2028 assert_eq!(stats.num_complete, -1);
2030 assert_eq!(stats.num_incomplete, -1);
2031 assert_eq!(stats.queue_position, -1);
2032 assert_eq!(stats.error_file, -1);
2033
2034 assert!(stats.current_tracker.is_empty());
2036 assert!(stats.save_path.is_empty());
2037 assert!(stats.error.is_empty());
2038
2039 assert_eq!(stats.download_rate, 0);
2041 assert_eq!(stats.upload_rate, 0);
2042 assert_eq!(stats.download_payload_rate, 0);
2043 assert_eq!(stats.upload_payload_rate, 0);
2044
2045 assert_eq!(stats.distributed_full_copies, 0);
2047 assert_eq!(stats.distributed_fraction, 0);
2048 assert!((stats.distributed_copies - 0.0).abs() < f32::EPSILON);
2049 }
2050
2051 #[test]
2052 fn torrent_stats_seeding_flags() {
2053 let stats = TorrentStats {
2054 state: TorrentState::Seeding,
2055 is_seeding: true,
2056 is_finished: true,
2057 has_metadata: true,
2058 progress: 1.0,
2059 progress_ppm: 1_000_000,
2060 ..Default::default()
2061 };
2062 assert_eq!(stats.state, TorrentState::Seeding);
2063 assert!(stats.is_seeding);
2064 assert!(stats.is_finished);
2065 assert!(stats.has_metadata);
2066 assert!((stats.progress - 1.0).abs() < f32::EPSILON);
2067 assert_eq!(stats.progress_ppm, 1_000_000);
2068 assert!(!stats.is_paused);
2070 assert_eq!(stats.downloaded, 0);
2071 }
2072
2073 #[test]
2074 fn torrent_state_sharing_variant() {
2075 let state = TorrentState::Sharing;
2076 assert_ne!(state, TorrentState::Downloading);
2077 assert_ne!(state, TorrentState::Seeding);
2078 let json = serde_json::to_string(&state).unwrap();
2080 assert_eq!(json, "\"Sharing\"");
2081 let decoded: TorrentState = serde_json::from_str(&json).unwrap();
2082 assert_eq!(decoded, TorrentState::Sharing);
2083 }
2084
2085 #[test]
2086 fn torrent_config_i2p_defaults() {
2087 let cfg = TorrentConfig::default();
2088 assert!(!cfg.enable_i2p);
2089 assert!(!cfg.allow_i2p_mixed);
2090 }
2091
2092 #[test]
2093 fn torrent_config_ssl_listen_port_default() {
2094 let cfg = TorrentConfig::default();
2095 assert_eq!(cfg.ssl_listen_port, 0);
2096 }
2097
2098 #[test]
2099 fn torrent_config_ssl_listen_port_from_settings() {
2100 let s = crate::settings::Settings {
2101 ssl_listen_port: 4433,
2102 ..crate::settings::Settings::default()
2103 };
2104 let tc = TorrentConfig::from(&s);
2105 assert_eq!(tc.ssl_listen_port, 4433);
2106 }
2107
2108 #[test]
2109 fn torrent_config_choking_defaults() {
2110 let cfg = TorrentConfig::default();
2111 assert_eq!(
2112 cfg.seed_choking_algorithm,
2113 SeedChokingAlgorithm::FastestUpload
2114 );
2115 assert_eq!(cfg.choking_algorithm, ChokingAlgorithm::FixedSlots);
2116 }
2117
2118 #[test]
2119 fn torrent_config_m44_defaults() {
2120 let cfg = TorrentConfig::default();
2121 assert!(cfg.piece_extent_affinity);
2122 assert!(!cfg.suggest_mode);
2123 assert_eq!(cfg.max_suggest_pieces, 10);
2124 assert_eq!(cfg.predictive_piece_announce_ms, 0);
2125 }
2126
2127 #[test]
2128 fn torrent_config_from_settings_choking() {
2129 let s = crate::settings::Settings {
2130 seed_choking_algorithm: SeedChokingAlgorithm::RoundRobin,
2131 choking_algorithm: ChokingAlgorithm::RateBased,
2132 ..crate::settings::Settings::default()
2133 };
2134 let cfg = TorrentConfig::from(&s);
2135 assert_eq!(cfg.seed_choking_algorithm, SeedChokingAlgorithm::RoundRobin);
2136 assert_eq!(cfg.choking_algorithm, ChokingAlgorithm::RateBased);
2137 }
2138
2139 #[test]
2140 fn torrent_config_holepunch_default() {
2141 let cfg = TorrentConfig::default();
2142 assert!(cfg.enable_holepunch);
2143
2144 let s = crate::settings::Settings::default();
2146 let tc = TorrentConfig::from(&s);
2147 assert!(tc.enable_holepunch);
2148
2149 let s2 = crate::settings::Settings {
2151 enable_holepunch: false,
2152 ..crate::settings::Settings::default()
2153 };
2154 let tc2 = TorrentConfig::from(&s2);
2155 assert!(!tc2.enable_holepunch);
2156 }
2157
2158 #[test]
2159 fn torrent_config_url_security_default() {
2160 let cfg = TorrentConfig::default();
2161 assert!(cfg.url_security.ssrf_mitigation);
2162 assert!(!cfg.url_security.allow_idna);
2163 assert!(cfg.url_security.validate_https_trackers);
2164 }
2165
2166 #[test]
2167 fn torrent_config_url_security_from_settings() {
2168 let s = crate::settings::Settings {
2169 ssrf_mitigation: false,
2170 allow_idna: true,
2171 validate_https_trackers: false,
2172 ..crate::settings::Settings::default()
2173 };
2174 let cfg = TorrentConfig::from(&s);
2175 assert!(!cfg.url_security.ssrf_mitigation);
2176 assert!(cfg.url_security.allow_idna);
2177 assert!(!cfg.url_security.validate_https_trackers);
2178 }
2179
2180 #[test]
2181 fn torrent_config_peer_dscp_default() {
2182 let cfg = TorrentConfig::default();
2183 assert_eq!(cfg.peer_dscp, 0x08);
2184 }
2185
2186 #[test]
2187 fn torrent_config_peer_dscp_from_settings() {
2188 let s = crate::settings::Settings {
2189 peer_dscp: 0x2E,
2190 ..crate::settings::Settings::default()
2191 };
2192 let cfg = TorrentConfig::from(&s);
2193 assert_eq!(cfg.peer_dscp, 0x2E);
2194 }
2195
2196 #[test]
2199 fn summary_from_stats() {
2200 let v1_hash =
2201 irontide_core::Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
2202 let stats = TorrentStats {
2203 info_hashes: irontide_core::InfoHashes::v1_only(v1_hash),
2204 name: "test torrent".to_string(),
2205 state: TorrentState::Downloading,
2206 progress: 0.75,
2207 download_rate: 1_000_000,
2208 upload_rate: 500_000,
2209 total: 100_000_000,
2210 num_peers: 42,
2211 added_time: 1_710_900_000,
2212 ..TorrentStats::default()
2213 };
2214
2215 let summary = super::TorrentSummary::from(&stats);
2216 assert_eq!(
2217 summary.info_hash,
2218 "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
2219 );
2220 assert_eq!(summary.name, "test torrent");
2221 assert_eq!(summary.state, TorrentState::Downloading);
2222 assert!((summary.progress - 0.75).abs() < f64::EPSILON);
2223 assert_eq!(summary.download_rate, 1_000_000);
2224 assert_eq!(summary.upload_rate, 500_000);
2225 assert_eq!(summary.total_size, 100_000_000);
2226 assert_eq!(summary.num_peers, 42);
2227 assert_eq!(summary.added_time, 1_710_900_000);
2228 }
2229
2230 #[test]
2231 fn summary_from_stats_v2_only() {
2232 let v2_hash = irontide_core::Id32::from_hex(
2233 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2234 )
2235 .unwrap();
2236 let stats = TorrentStats {
2237 info_hashes: irontide_core::InfoHashes::v2_only(v2_hash),
2238 name: "v2 torrent".to_string(),
2239 ..TorrentStats::default()
2240 };
2241
2242 let summary = super::TorrentSummary::from(&stats);
2243 assert_eq!(summary.info_hash, "");
2245 assert_eq!(summary.name, "v2 torrent");
2246 }
2247
2248 #[test]
2249 fn stats_serializable() {
2250 let stats = TorrentStats::default();
2251 let json = serde_json::to_string(&stats).expect("TorrentStats should serialize to JSON");
2252 assert!(json.contains("\"state\""));
2253 assert!(json.contains("\"info_hashes\""));
2254 assert!(json.contains("\"download_rate\""));
2255 }
2256
2257 #[test]
2258 fn info_hashes_serializable() {
2259 let v1 = irontide_core::Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
2260 let ih = irontide_core::InfoHashes::v1_only(v1);
2261 let json = serde_json::to_string(&ih).expect("InfoHashes should serialize to JSON");
2262 assert!(json.contains("\"v1\""));
2264 assert!(json.contains("\"v2\":null"));
2265 }
2266
2267 #[test]
2268 fn summary_serializable() {
2269 let stats = TorrentStats {
2270 name: "serialize test".to_string(),
2271 state: TorrentState::Seeding,
2272 progress: 1.0,
2273 ..TorrentStats::default()
2274 };
2275 let summary = super::TorrentSummary::from(&stats);
2276 let json =
2277 serde_json::to_string(&summary).expect("TorrentSummary should serialize to JSON");
2278 assert!(json.contains("\"name\":\"serialize test\""));
2279 assert!(json.contains("\"state\":\"Seeding\""));
2280 assert!(json.contains("\"progress\":1.0"));
2281 }
2282
2283 #[test]
2284 fn test_torrent_summary_includes_seeds_and_totals() {
2285 let stats = TorrentStats {
2286 num_seeds: 7,
2287 all_time_upload: 1_500_000,
2288 all_time_download: 3_000_000,
2289 ..TorrentStats::default()
2290 };
2291
2292 let summary = super::TorrentSummary::from(&stats);
2293 assert_eq!(summary.num_seeds, 7);
2294 assert_eq!(summary.all_time_upload, 1_500_000);
2295 assert_eq!(summary.all_time_download, 3_000_000);
2296 }
2297
2298 #[test]
2299 fn settings_delta_empty_when_identical() {
2300 let s = crate::settings::Settings::default();
2301 let delta = SettingsDelta::from_diff(&s, &s);
2302 assert!(delta.is_empty());
2303 }
2304
2305 #[test]
2306 fn settings_delta_detects_dht_change() {
2307 let old = crate::settings::Settings::default();
2308 let mut new = old.clone();
2309 new.enable_dht = !old.enable_dht;
2310 let delta = SettingsDelta::from_diff(&old, &new);
2311 assert!(!delta.is_empty());
2312 assert_eq!(delta.enable_dht, Some(new.enable_dht));
2313 assert!(delta.enable_pex.is_none());
2314 }
2315
2316 #[test]
2317 fn settings_delta_detects_seed_ratio_change() {
2318 let old = crate::settings::Settings::default();
2319 let mut new = old.clone();
2320 new.seed_ratio_limit = Some(2.0);
2321 let delta = SettingsDelta::from_diff(&old, &new);
2322 assert!(!delta.is_empty());
2323 assert_eq!(delta.seed_ratio_limit, Some(Some(2.0)));
2324 }
2325
2326 #[test]
2327 fn settings_delta_detects_encryption_change() {
2328 let old = crate::settings::Settings::default();
2329 let mut new = old.clone();
2330 new.encryption_mode = irontide_wire::mse::EncryptionMode::Forced;
2331 let delta = SettingsDelta::from_diff(&old, &new);
2332 assert!(!delta.is_empty());
2333 assert_eq!(
2334 delta.encryption_mode,
2335 Some(irontide_wire::mse::EncryptionMode::Forced)
2336 );
2337 }
2338
2339 #[test]
2340 fn settings_delta_detects_max_peers_change() {
2341 let old = crate::settings::Settings::default();
2342 let mut new = old.clone();
2343 new.max_peers_per_torrent = 0;
2344 let delta = SettingsDelta::from_diff(&old, &new);
2345 assert!(!delta.is_empty());
2346 assert_eq!(delta.max_peers, Some(0));
2347 }
2348
2349 #[test]
2350 fn settings_delta_detects_max_uploads_per_torrent_change() {
2351 let old = crate::settings::Settings::default();
2352 let mut new = old.clone();
2353 new.max_uploads_per_torrent = 6;
2354 let delta = SettingsDelta::from_diff(&old, &new);
2355 assert!(!delta.is_empty());
2356 assert_eq!(delta.max_uploads_per_torrent, Some(6));
2357 assert!(delta.max_peers.is_none());
2358 }
2359
2360 #[test]
2361 fn settings_delta_detects_max_uploads_per_torrent_unlimited_change() {
2362 let old = crate::settings::Settings {
2363 max_uploads_per_torrent: 4,
2364 ..crate::settings::Settings::default()
2365 };
2366 let mut new = old.clone();
2367 new.max_uploads_per_torrent = -1;
2368 let delta = SettingsDelta::from_diff(&old, &new);
2369 assert!(!delta.is_empty());
2370 assert_eq!(delta.max_uploads_per_torrent, Some(-1));
2371 }
2372
2373 #[test]
2374 fn torrent_config_from_settings_propagates_max_uploads_per_torrent() {
2375 let mut s = crate::settings::Settings {
2376 max_uploads_per_torrent: 7,
2377 ..crate::settings::Settings::default()
2378 };
2379 let cfg = TorrentConfig::from(&s);
2380 assert_eq!(cfg.max_uploads_per_torrent, 7);
2381
2382 s.max_uploads_per_torrent = -1;
2383 let cfg = TorrentConfig::from(&s);
2384 assert_eq!(cfg.max_uploads_per_torrent, -1);
2385 }
2386
2387 #[test]
2388 fn torrent_stats_holepunch_relayed_default_zero() {
2389 let stats = TorrentStats::default();
2390 assert_eq!(stats.holepunch_relayed, 0);
2391 }
2392
2393 #[test]
2394 fn torrent_stats_holepunch_relayed_serializes() {
2395 let stats = TorrentStats::default();
2396 let json = serde_json::to_value(&stats).unwrap();
2397 assert_eq!(json["holepunch_relayed"], 0);
2398 }
2399}