Skip to main content

irontide_session/
types.rs

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/// Configurable parameters for a torrent session.
16#[derive(Debug, Clone)]
17pub struct TorrentConfig {
18    /// TCP listen port for incoming peer connections.
19    pub listen_port: u16,
20    /// Maximum number of peer connections per torrent.
21    pub max_peers: usize,
22    /// Number of outstanding piece requests to maintain per peer.
23    pub target_request_queue: usize,
24    /// Directory where downloaded files are stored.
25    pub download_dir: PathBuf,
26    /// Enable DHT for peer discovery.
27    pub enable_dht: bool,
28    /// Enable Peer Exchange (BEP 11) for peer discovery.
29    pub enable_pex: bool,
30    /// Enable Fast Extension (BEP 6) for reject/suggest/allowed-fast messages.
31    pub enable_fast: bool,
32    /// Stop seeding after reaching this upload/download ratio (None = unlimited).
33    pub seed_ratio_limit: Option<f64>,
34    /// In end game mode, cancel duplicate requests when a piece completes.
35    pub strict_end_game: bool,
36    /// Upload rate limit in bytes/sec (0 = unlimited).
37    pub upload_rate_limit: u64,
38    /// Download rate limit in bytes/sec (0 = unlimited).
39    pub download_rate_limit: u64,
40    /// Connection encryption mode (MSE/PE).
41    pub encryption_mode: irontide_wire::mse::EncryptionMode,
42    /// Enable uTP (micro Transport Protocol) for peer connections.
43    pub enable_utp: bool,
44    /// Enable HTTP/web seeding (BEP 19, BEP 17).
45    pub enable_web_seed: bool,
46    /// Enable BEP 55 holepunch extension for NAT traversal.
47    pub enable_holepunch: bool,
48    /// Enable BEP 40 canonical peer priority for connection eviction.
49    pub enable_bep40_eviction: bool,
50    /// Maximum concurrent web seed connections.
51    pub max_web_seeds: usize,
52    /// BEP 16: super seeding mode — reveal pieces one-per-peer for maximum diversity.
53    pub super_seeding: bool,
54    /// BEP 21: advertise upload-only status via extension handshake when seeding.
55    pub upload_only_announce: bool,
56    /// Number of concurrent piece verifications during torrent checking.
57    pub hashing_threads: usize,
58    /// Enable sequential (in-order) piece downloading.
59    pub sequential_download: bool,
60    /// Completed piece count below which the picker uses random selection to promote diversity.
61    pub initial_picker_threshold: u32,
62    /// Seconds below which a fast peer downloads a whole piece; if under this, picker grants
63    /// exclusive assignment (no block splitting).
64    pub whole_pieces_threshold: u32,
65    /// Seconds without data from a peer before marking it as snubbed.
66    pub snub_timeout_secs: u32,
67    /// Number of pieces ahead of the streaming cursor to prioritize.
68    pub readahead_pieces: u32,
69    /// When true, escalate streaming piece requests that exceed the mean RTT.
70    pub streaming_timeout_escalation: bool,
71    /// Maximum concurrent file stream readers per torrent.
72    pub max_concurrent_stream_reads: usize,
73    /// Proxy configuration for outbound peer connections.
74    pub proxy: crate::proxy::ProxyConfig,
75    /// Anonymous mode: suppress client identity in peer handshakes.
76    pub anonymous_mode: bool,
77    /// Share mode: relay pieces in memory without writing to disk.
78    /// Requires `enable_fast` for RejectRequest when evicting pieces.
79    pub share_mode: bool,
80    /// Whether this torrent should use I2P for peer connections.
81    pub enable_i2p: bool,
82    /// Whether to allow mixing I2P and clearnet peers.
83    pub allow_i2p_mixed: bool,
84    /// SSL listen port for SSL torrent connections (0 = disabled).
85    pub ssl_listen_port: u16,
86    /// Algorithm for ranking peers during seed-mode choking.
87    pub seed_choking_algorithm: SeedChokingAlgorithm,
88    /// Algorithm for determining the number of unchoke slots.
89    pub choking_algorithm: ChokingAlgorithm,
90    /// Prefer grouping piece requests within the same 4 MiB disk extent.
91    pub piece_extent_affinity: bool,
92    /// Enable sending SuggestPiece messages for cached pieces.
93    pub suggest_mode: bool,
94    /// Maximum number of pieces to suggest per peer.
95    pub max_suggest_pieces: usize,
96    /// Delay (ms) before announcing Have for a piece still being written to disk (0 = disabled).
97    pub predictive_piece_announce_ms: u64,
98    /// Mixed-mode TCP/uTP bandwidth allocation algorithm.
99    pub mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm,
100    /// Enable automatic sequential mode switching on partial-piece explosion.
101    pub auto_sequential: bool,
102    /// Storage allocation mode for disk I/O.
103    pub storage_mode: irontide_core::StorageMode,
104    /// Override pre-allocation strategy. `None` = derive from `storage_mode`.
105    pub preallocate_mode: Option<irontide_storage::PreallocateMode>,
106    /// Block request timeout in seconds before re-issuing (0 = disabled).
107    pub block_request_timeout_secs: u32,
108    /// Enable Local Service Discovery (BEP 14) for this torrent.
109    pub enable_lsd: bool,
110    /// Force all connections through the configured proxy.
111    pub force_proxy: bool,
112    /// Steal blocks from peers this many times slower than the requesting peer (0.0 = disabled).
113    pub steal_threshold_ratio: f64,
114    /// M149: Steal threshold multiplier when >90% complete.
115    pub steal_threshold_endgame: f64,
116    /// M133: Seconds without any wire message before disconnecting a peer (0 = disabled).
117    pub peer_read_timeout_secs: u64,
118    /// M133: Seconds before a stalled outgoing write disconnects a peer (0 = disabled).
119    pub peer_write_timeout_secs: u64,
120    /// M137: Seconds without Piece data before disconnecting a peer (0 = disabled).
121    pub data_contribution_timeout_secs: u64,
122    /// M138: Maximum peers to evict per choke rotation tick (0 = disabled).
123    pub choke_rotation_max_evictions: u32,
124    /// M138: Maximum concurrent outbound peer connections.
125    pub max_concurrent_connects: u16,
126    /// M147: Seconds without TCP SYN-ACK before soft reap disconnects.
127    pub connect_soft_timeout: u64,
128    /// URL security configuration for SSRF mitigation and IDNA checking.
129    pub url_security: crate::url_guard::UrlSecurityConfig,
130    /// Timeout in seconds for outbound TCP peer connections (0 = OS default).
131    pub peer_connect_timeout: u64,
132    /// DSCP (Differentiated Services Code Point) value for peer traffic sockets.
133    pub peer_dscp: u8,
134    /// Fixed per-peer request queue depth for the lifetime of the connection.
135    pub initial_queue_depth: usize,
136    /// Maximum per-peer request queue depth.
137    pub max_request_queue_depth: usize,
138    /// Deprecated — unused in the fixed-depth pipeline model. Retained for API
139    /// compatibility; was formerly used to scale BDP-based queue depth.
140    pub request_queue_time: f64,
141    /// Maximum BEP 9 metadata size in bytes accepted from peers.
142    pub max_metadata_size: u64,
143    /// Maximum wire protocol message size in bytes for the codec.
144    pub max_message_size: usize,
145    /// Maximum accepted piece length when adding a torrent.
146    pub max_piece_length: u64,
147    /// Maximum outstanding incoming requests per peer.
148    pub max_outstanding_requests: usize,
149    /// Maximum number of pieces simultaneously in-flight (downloaded but not
150    /// yet verified).
151    pub max_in_flight_pieces: usize,
152    /// M149: Minimum per-peer pipeline depth (requests in flight).
153    pub min_pipeline_depth: u32,
154    /// M149: Maximum per-peer pipeline depth (requests in flight).
155    pub max_pipeline_depth: u32,
156    /// M149: Seconds of data to buffer in the pipeline per peer.
157    pub target_buffer_secs: f64,
158    /// M103: Enable block-level stealing for partially-downloaded pieces.
159    pub use_block_stealing: bool,
160    /// M132: Seconds between steal-queue population scans (0 = disabled).
161    pub steal_stale_piece_secs: u64,
162    /// M104: Fixed per-peer pipeline depth (concurrent requests per peer).
163    pub fixed_pipeline_depth: usize,
164    /// M120: Lock timing warning threshold in milliseconds (0 = disabled).
165    pub lock_warn_threshold_ms: u64,
166    /// M127: Enable direct I/O for filesystem storage (O_DIRECT / F_NOCACHE).
167    pub filesystem_direct_io: bool,
168}
169
170impl Default for TorrentConfig {
171    fn default() -> Self {
172        Self {
173            listen_port: 6881,
174            max_peers: 128,
175            target_request_queue: 5,
176            download_dir: PathBuf::from("."),
177            enable_dht: true,
178            enable_pex: true,
179            enable_fast: false,
180            seed_ratio_limit: None,
181            strict_end_game: true,
182            upload_rate_limit: 0,
183            download_rate_limit: 0,
184            encryption_mode: irontide_wire::mse::EncryptionMode::Disabled,
185            enable_utp: true,
186            enable_web_seed: true,
187            enable_holepunch: true,
188            enable_bep40_eviction: true,
189            max_web_seeds: 4,
190            super_seeding: false,
191            upload_only_announce: true,
192            hashing_threads: {
193                let cores = std::thread::available_parallelism()
194                    .map(|n| n.get())
195                    .unwrap_or(4);
196                (cores / 4).clamp(2, 8)
197            },
198            sequential_download: false,
199            initial_picker_threshold: 4,
200            whole_pieces_threshold: 20,
201            snub_timeout_secs: 15,
202            readahead_pieces: 8,
203            streaming_timeout_escalation: true,
204            max_concurrent_stream_reads: 8,
205            proxy: crate::proxy::ProxyConfig::default(),
206            anonymous_mode: false,
207            share_mode: false,
208            enable_i2p: false,
209            allow_i2p_mixed: false,
210            ssl_listen_port: 0,
211            seed_choking_algorithm: SeedChokingAlgorithm::FastestUpload,
212            choking_algorithm: ChokingAlgorithm::FixedSlots,
213            piece_extent_affinity: true,
214            suggest_mode: false,
215            max_suggest_pieces: 10,
216            predictive_piece_announce_ms: 0,
217            mixed_mode_algorithm: crate::rate_limiter::MixedModeAlgorithm::PeerProportional,
218            auto_sequential: true,
219            storage_mode: irontide_core::StorageMode::Auto,
220            preallocate_mode: None,
221            block_request_timeout_secs: 60,
222            enable_lsd: true,
223            force_proxy: false,
224            steal_threshold_ratio: 10.0,
225            steal_threshold_endgame: 3.0,
226            min_pipeline_depth: 16,
227            max_pipeline_depth: 512,
228            target_buffer_secs: 2.0,
229            peer_read_timeout_secs: 10,
230            peer_write_timeout_secs: 10,
231            data_contribution_timeout_secs: 0,
232            choke_rotation_max_evictions: 0,
233            max_concurrent_connects: 128,
234            connect_soft_timeout: 3,
235            url_security: crate::url_guard::UrlSecurityConfig::default(),
236            peer_connect_timeout: 10,
237            peer_dscp: 0x08,
238            initial_queue_depth: 128,
239            max_request_queue_depth: 250,
240            request_queue_time: 3.0,
241            max_metadata_size: 4 * 1024 * 1024,
242            max_message_size: 16 * 1024 * 1024,
243            max_piece_length: 32 * 1024 * 1024,
244            max_outstanding_requests: 500,
245            max_in_flight_pieces: 512,
246            use_block_stealing: true,
247            steal_stale_piece_secs: 2,
248            fixed_pipeline_depth: 128,
249            lock_warn_threshold_ms: 50,
250            filesystem_direct_io: false,
251        }
252    }
253}
254
255impl From<&crate::settings::Settings> for TorrentConfig {
256    fn from(s: &crate::settings::Settings) -> Self {
257        Self {
258            listen_port: 0, // Each torrent gets a random port (matches make_torrent_config)
259            max_peers: s.max_peers_per_torrent,
260            target_request_queue: 5,
261            download_dir: s.download_dir.clone(),
262            enable_dht: s.enable_dht,
263            enable_pex: s.enable_pex,
264            enable_fast: s.enable_fast_extension,
265            seed_ratio_limit: s.seed_ratio_limit,
266            strict_end_game: s.strict_end_game,
267            upload_rate_limit: s.upload_rate_limit,
268            download_rate_limit: s.download_rate_limit,
269            encryption_mode: s.encryption_mode,
270            enable_utp: s.enable_utp,
271            enable_web_seed: s.enable_web_seed,
272            enable_holepunch: s.enable_holepunch,
273            enable_bep40_eviction: s.enable_bep40_eviction,
274            max_web_seeds: s.max_web_seeds,
275            super_seeding: s.default_super_seeding,
276            upload_only_announce: s.upload_only_announce,
277            hashing_threads: s.hashing_threads,
278            sequential_download: false,
279            initial_picker_threshold: s.initial_picker_threshold,
280            whole_pieces_threshold: s.whole_pieces_threshold,
281            snub_timeout_secs: s.snub_timeout_secs,
282            readahead_pieces: s.readahead_pieces,
283            streaming_timeout_escalation: s.streaming_timeout_escalation,
284            max_concurrent_stream_reads: s.max_concurrent_stream_reads,
285            proxy: s.proxy.clone(),
286            anonymous_mode: s.anonymous_mode,
287            share_mode: s.default_share_mode,
288            enable_i2p: s.enable_i2p,
289            allow_i2p_mixed: s.allow_i2p_mixed,
290            ssl_listen_port: s.ssl_listen_port,
291            seed_choking_algorithm: s.seed_choking_algorithm,
292            choking_algorithm: s.choking_algorithm,
293            piece_extent_affinity: s.piece_extent_affinity,
294            suggest_mode: s.suggest_mode,
295            max_suggest_pieces: s.max_suggest_pieces,
296            predictive_piece_announce_ms: s.predictive_piece_announce_ms,
297            mixed_mode_algorithm: s.mixed_mode_algorithm,
298            auto_sequential: s.auto_sequential,
299            storage_mode: s.storage_mode,
300            preallocate_mode: s.preallocate_mode,
301            block_request_timeout_secs: s.block_request_timeout_secs,
302            enable_lsd: s.enable_lsd,
303            force_proxy: s.force_proxy,
304            steal_threshold_ratio: s.steal_threshold_ratio,
305            steal_threshold_endgame: s.steal_threshold_endgame,
306            min_pipeline_depth: s.min_pipeline_depth,
307            max_pipeline_depth: s.max_pipeline_depth,
308            target_buffer_secs: s.target_buffer_secs,
309            peer_read_timeout_secs: s.peer_read_timeout_secs,
310            peer_write_timeout_secs: s.peer_write_timeout_secs,
311            data_contribution_timeout_secs: s.data_contribution_timeout_secs,
312            choke_rotation_max_evictions: s.choke_rotation_max_evictions,
313            max_concurrent_connects: s.max_concurrent_connects,
314            connect_soft_timeout: s.connect_soft_timeout,
315            url_security: crate::url_guard::UrlSecurityConfig::from(s),
316            peer_connect_timeout: s.peer_connect_timeout,
317            peer_dscp: s.peer_dscp,
318            initial_queue_depth: s.initial_queue_depth,
319            max_request_queue_depth: s.max_request_queue_depth,
320            request_queue_time: s.request_queue_time,
321            max_metadata_size: s.max_metadata_size,
322            max_message_size: s.max_message_size,
323            max_piece_length: s.max_piece_length,
324            max_outstanding_requests: s.max_outstanding_requests,
325            max_in_flight_pieces: s.max_in_flight_pieces,
326            use_block_stealing: s.use_block_stealing,
327            steal_stale_piece_secs: s.steal_stale_piece_secs,
328            fixed_pipeline_depth: s.fixed_pipeline_depth,
329            lock_warn_threshold_ms: s.lock_warn_threshold_ms,
330            filesystem_direct_io: s.filesystem_direct_io,
331        }
332    }
333}
334
335/// Current state of a torrent.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337pub enum TorrentState {
338    /// Waiting for peers to send torrent metadata via BEP 9.
339    FetchingMetadata,
340    /// Verifying existing data on disk against piece hashes.
341    Checking,
342    /// Actively downloading pieces from peers.
343    Downloading,
344    /// All pieces downloaded, awaiting transition to seeding.
345    Complete,
346    /// Upload-only: all pieces verified, serving to other peers.
347    Seeding,
348    /// Manually paused by the user. No peer connections maintained.
349    Paused,
350    /// Removed from the session. Terminal state.
351    Stopped,
352    /// Share mode: relay pieces in memory without writing to disk.
353    Sharing,
354}
355
356/// Aggregate statistics for a torrent.
357#[derive(Debug, Clone, Serialize)]
358pub struct TorrentStats {
359    // ── Original fields (unchanged) ──
360    /// Current torrent state.
361    pub state: TorrentState,
362    /// Total bytes downloaded (payload only).
363    pub downloaded: u64,
364    /// Total bytes uploaded (payload only).
365    pub uploaded: u64,
366    /// Number of pieces that have been verified.
367    pub pieces_have: u32,
368    /// Total number of pieces in the torrent.
369    pub pieces_total: u32,
370    /// Number of currently connected peers.
371    pub peers_connected: usize,
372    /// Number of known peers (connected + available).
373    pub peers_available: usize,
374    /// Progress of piece checking (0.0–1.0), meaningful when state is `Checking`.
375    pub checking_progress: f32,
376    /// Number of connected peers broken down by discovery source.
377    pub peers_by_source: HashMap<crate::peer_state::PeerSource, usize>,
378
379    // ── Identity ──
380    /// Info hashes (v1 SHA-1 and/or v2 SHA-256) for this torrent.
381    pub info_hashes: irontide_core::InfoHashes,
382    /// Display name from the torrent metadata.
383    pub name: String,
384
385    // ── State flags ──
386    /// Whether metadata has been received (always true for .torrent adds).
387    pub has_metadata: bool,
388    /// Whether we have all pieces and are seeding.
389    pub is_seeding: bool,
390    /// Whether all wanted pieces are downloaded (may differ from is_seeding with file priorities).
391    pub is_finished: bool,
392    /// Whether the torrent is paused.
393    pub is_paused: bool,
394    /// Whether the torrent is auto-managed by the session queuing system.
395    pub auto_managed: bool,
396    /// Whether sequential piece downloading is enabled.
397    pub sequential_download: bool,
398    /// Whether BEP 16 super seeding mode is active.
399    pub super_seeding: bool,
400    /// Whether the user explicitly toggled seed-only mode (M159).
401    ///
402    /// Distinct from `is_seeding` which reflects download completion.
403    /// When `true`, the engine stops scheduling new block requests but
404    /// continues to serve uploads to interested peers.
405    #[serde(default)]
406    pub user_seed_mode: bool,
407    /// Whether we have accepted any incoming peer connections.
408    pub has_incoming: bool,
409    /// Whether resume data needs to be saved.
410    pub need_save_resume: bool,
411    /// Whether a storage move operation is in progress.
412    pub moving_storage: bool,
413
414    // ── Progress ──
415    /// Download progress as a fraction (0.0–1.0).
416    pub progress: f32,
417    /// Download progress in parts per million (0–1_000_000).
418    pub progress_ppm: u32,
419    /// Total bytes of verified (downloaded and hash-checked) data.
420    pub total_done: u64,
421    /// Total size of the torrent in bytes.
422    pub total: u64,
423    /// Total bytes of wanted data that have been verified.
424    pub total_wanted_done: u64,
425    /// Total bytes of wanted data (respecting file priorities).
426    pub total_wanted: u64,
427    /// Block (sub-piece request) size in bytes.
428    pub block_size: u32,
429
430    // ── Transfer (session counters) ──
431    /// Total bytes downloaded this session (including protocol overhead).
432    pub total_download: u64,
433    /// Total bytes uploaded this session (including protocol overhead).
434    pub total_upload: u64,
435    /// Total payload bytes downloaded this session.
436    pub total_payload_download: u64,
437    /// Total payload bytes uploaded this session.
438    pub total_payload_upload: u64,
439    /// Total bytes of data that failed hash check.
440    pub total_failed_bytes: u64,
441    /// Total bytes of redundant (duplicate) data received.
442    pub total_redundant_bytes: u64,
443
444    // ── Transfer (all-time, persisted) ──
445    /// All-time total bytes downloaded (persisted across sessions via resume data).
446    pub all_time_download: u64,
447    /// All-time total bytes uploaded (persisted across sessions via resume data).
448    pub all_time_upload: u64,
449
450    // ── Rates ──
451    /// Current download rate in bytes/sec (including protocol overhead).
452    pub download_rate: u64,
453    /// Current upload rate in bytes/sec (including protocol overhead).
454    pub upload_rate: u64,
455    /// Current payload download rate in bytes/sec.
456    pub download_payload_rate: u64,
457    /// Current payload upload rate in bytes/sec.
458    pub upload_payload_rate: u64,
459
460    // ── Connection details ──
461    /// Number of peers connected (including half-open).
462    pub num_peers: usize,
463    /// Number of connected peers that are seeds.
464    pub num_seeds: usize,
465    /// Number of complete copies known from tracker scrape (-1 = unknown).
466    pub num_complete: i32,
467    /// Number of incomplete copies known from tracker scrape (-1 = unknown).
468    pub num_incomplete: i32,
469    /// Total number of seeds across all trackers.
470    pub list_seeds: usize,
471    /// Total number of peers across all trackers.
472    pub list_peers: usize,
473    /// Number of peers available to connect to (not yet connected).
474    pub connect_candidates: usize,
475    /// Number of active peer connections (TCP + uTP).
476    pub num_connections: usize,
477    /// Number of unchoked peers we are uploading to.
478    pub num_uploads: usize,
479    /// M133: Total unique peer connection attempts during this session.
480    pub unique_peers_attempted: u64,
481    /// M137: Peer pipeline lifecycle snapshot.
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub pipeline: Option<crate::peer_states::PeerPipelineSnapshot>,
484    /// M138: Total peers evicted by proactive choke rotation.
485    pub choke_rotations: u64,
486    /// M149: Total number of piece-level steals performed.
487    pub piece_steals: u64,
488
489    // ── Limits ──
490    /// Maximum number of connections for this torrent.
491    pub connections_limit: usize,
492    /// Maximum number of unchoke slots for this torrent.
493    pub uploads_limit: usize,
494
495    // ── Distributed copies ──
496    /// Number of full distributed copies available in the swarm.
497    pub distributed_full_copies: u32,
498    /// Fractional part of distributed copies (0–999).
499    pub distributed_fraction: u32,
500    /// Distributed copies as a float (full + fraction/1000).
501    pub distributed_copies: f32,
502
503    // ── Tracker ──
504    /// URL of the tracker we most recently announced to.
505    pub current_tracker: String,
506    /// Whether we are currently announcing to any tracker.
507    pub announcing_to_trackers: bool,
508    /// Whether we are currently announcing to LSD (Local Service Discovery).
509    pub announcing_to_lsd: bool,
510    /// Whether we are currently announcing to DHT.
511    pub announcing_to_dht: bool,
512
513    // ── Timestamps (POSIX seconds) ──
514    /// Time when the torrent was added to the session.
515    pub added_time: i64,
516    /// Time when the torrent completed downloading (0 = not completed).
517    pub completed_time: i64,
518    /// Last time a complete copy was seen in the swarm (0 = never).
519    pub last_seen_complete: i64,
520    /// Time of last upload activity (0 = never).
521    pub last_upload: i64,
522    /// Time of last download activity (0 = never).
523    pub last_download: i64,
524
525    // ── Durations (cumulative seconds) ──
526    /// Total seconds the torrent has been active (downloading or seeding).
527    pub active_duration: i64,
528    /// Total seconds the torrent has been in finished state.
529    pub finished_duration: i64,
530    /// Total seconds the torrent has been seeding.
531    pub seeding_duration: i64,
532
533    // ── Storage ──
534    /// Current save path for the torrent data.
535    pub save_path: String,
536
537    // ── Queue ──
538    /// Position in the session queue (-1 = not queued).
539    pub queue_position: i32,
540
541    // ── Error ──
542    /// Human-readable error message (empty = no error).
543    pub error: String,
544    /// Index of the file that caused the error (-1 = not file-specific).
545    pub error_file: i32,
546}
547
548impl Default for TorrentStats {
549    fn default() -> Self {
550        Self {
551            // Original fields
552            state: TorrentState::Paused,
553            downloaded: 0,
554            uploaded: 0,
555            pieces_have: 0,
556            pieces_total: 0,
557            peers_connected: 0,
558            peers_available: 0,
559            checking_progress: 0.0,
560            peers_by_source: HashMap::new(),
561
562            // Identity
563            info_hashes: irontide_core::InfoHashes::v1_only(irontide_core::Id20::from([0u8; 20])),
564            name: String::new(),
565
566            // State flags
567            has_metadata: false,
568            is_seeding: false,
569            is_finished: false,
570            is_paused: false,
571            auto_managed: false,
572            sequential_download: false,
573            super_seeding: false,
574            user_seed_mode: false,
575            has_incoming: false,
576            need_save_resume: false,
577            moving_storage: false,
578
579            // Progress
580            progress: 0.0,
581            progress_ppm: 0,
582            total_done: 0,
583            total: 0,
584            total_wanted_done: 0,
585            total_wanted: 0,
586            block_size: 16384,
587
588            // Transfer (session counters)
589            total_download: 0,
590            total_upload: 0,
591            total_payload_download: 0,
592            total_payload_upload: 0,
593            total_failed_bytes: 0,
594            total_redundant_bytes: 0,
595
596            // Transfer (all-time)
597            all_time_download: 0,
598            all_time_upload: 0,
599
600            // Rates
601            download_rate: 0,
602            upload_rate: 0,
603            download_payload_rate: 0,
604            upload_payload_rate: 0,
605
606            // Connection details
607            num_peers: 0,
608            num_seeds: 0,
609            num_complete: -1,
610            num_incomplete: -1,
611            list_seeds: 0,
612            list_peers: 0,
613            connect_candidates: 0,
614            num_connections: 0,
615            num_uploads: 0,
616            unique_peers_attempted: 0,
617            pipeline: None,
618            choke_rotations: 0,
619            piece_steals: 0,
620
621            // Limits
622            connections_limit: 0,
623            uploads_limit: 0,
624
625            // Distributed copies
626            distributed_full_copies: 0,
627            distributed_fraction: 0,
628            distributed_copies: 0.0,
629
630            // Tracker
631            current_tracker: String::new(),
632            announcing_to_trackers: false,
633            announcing_to_lsd: false,
634            announcing_to_dht: false,
635
636            // Timestamps
637            added_time: 0,
638            completed_time: 0,
639            last_seen_complete: 0,
640            last_upload: 0,
641            last_download: 0,
642
643            // Durations
644            active_duration: 0,
645            finished_duration: 0,
646            seeding_duration: 0,
647
648            // Storage
649            save_path: String::new(),
650
651            // Queue
652            queue_position: -1,
653
654            // Error
655            error: String::new(),
656            error_file: -1,
657        }
658    }
659}
660
661/// Lightweight summary of a torrent for the HTTP API.
662///
663/// A reduced view of [`TorrentStats`] with only the fields needed for list views.
664/// Constructed via `From<&TorrentStats>`.
665#[derive(Debug, Clone, Serialize)]
666pub struct TorrentSummary {
667    /// Hex-encoded v1 info hash (empty string for v2-only torrents).
668    pub info_hash: String,
669    /// Display name from the torrent metadata.
670    pub name: String,
671    /// Current torrent state.
672    pub state: TorrentState,
673    /// Download progress as a fraction (0.0–1.0).
674    pub progress: f64,
675    /// Current download rate in bytes/sec.
676    pub download_rate: u64,
677    /// Current upload rate in bytes/sec.
678    pub upload_rate: u64,
679    /// Total size of the torrent in bytes.
680    pub total_size: u64,
681    /// Number of connected peers.
682    pub num_peers: usize,
683    /// Number of connected seeders.
684    pub num_seeds: usize,
685    /// Total bytes uploaded across all sessions.
686    pub all_time_upload: u64,
687    /// Total bytes downloaded across all sessions.
688    pub all_time_download: u64,
689    /// Time when the torrent was added (POSIX seconds).
690    pub added_time: i64,
691    /// Whether the user has enabled seed-only mode for this torrent.
692    pub user_seed_mode: bool,
693    /// Progress of piece checking (0.0–1.0), meaningful when state is `Checking`.
694    pub checking_progress: f32,
695}
696
697impl From<&TorrentStats> for TorrentSummary {
698    fn from(s: &TorrentStats) -> Self {
699        Self {
700            info_hash: s.info_hashes.v1.map(|h| h.to_hex()).unwrap_or_default(),
701            name: s.name.clone(),
702            state: s.state,
703            progress: s.progress as f64,
704            download_rate: s.download_rate,
705            upload_rate: s.upload_rate,
706            total_size: s.total,
707            num_peers: s.num_peers,
708            num_seeds: s.num_seeds,
709            all_time_upload: s.all_time_upload,
710            all_time_download: s.all_time_download,
711            added_time: s.added_time,
712            user_seed_mode: s.user_seed_mode,
713            checking_progress: s.checking_progress,
714        }
715    }
716}
717
718/// Lightweight record of a single block write completion,
719/// carried in `PeerEvent::PieceBlocksBatch`.
720#[derive(Debug, Clone)]
721pub(crate) struct BlockEntry {
722    pub index: u32,  // piece index
723    pub begin: u32,  // byte offset within piece
724    pub length: u32, // block size (usually 16384)
725}
726
727/// Events sent from a `PeerTask` back to the `TorrentActor`.
728#[derive(Debug)]
729#[allow(dead_code)] // consumed by peer/torrent modules (not yet implemented)
730pub(crate) enum PeerEvent {
731    Bitfield {
732        peer_addr: SocketAddr,
733        bitfield: Bitfield,
734    },
735    Have {
736        peer_addr: SocketAddr,
737        index: u32,
738    },
739    /// BEP 54: Peer no longer has a piece (lt_donthave extension).
740    DontHave {
741        peer_addr: SocketAddr,
742        index: u32,
743    },
744    PieceData {
745        peer_addr: SocketAddr,
746        index: u32,
747        begin: u32,
748        data: Bytes,
749    },
750    /// Block completion from a peer task's direct disk write.
751    /// Sent immediately on each block write for real-time TorrentActor visibility.
752    PieceBlocksBatch {
753        peer_addr: SocketAddr,
754        blocks: Vec<BlockEntry>,
755    },
756    PeerChoking {
757        peer_addr: SocketAddr,
758        choking: bool,
759    },
760    PeerInterested {
761        peer_addr: SocketAddr,
762        interested: bool,
763    },
764    ExtHandshake {
765        peer_addr: SocketAddr,
766        handshake: ExtHandshake,
767    },
768    MetadataPiece {
769        peer_addr: SocketAddr,
770        piece: u32,
771        data: Bytes,
772        total_size: u64,
773    },
774    MetadataReject {
775        peer_addr: SocketAddr,
776        piece: u32,
777    },
778    PexPeers {
779        new_peers: Vec<SocketAddr>,
780    },
781    TrackersReceived {
782        tracker_urls: Vec<String>,
783    },
784    IncomingRequest {
785        peer_addr: SocketAddr,
786        index: u32,
787        begin: u32,
788        length: u32,
789    },
790    RejectRequest {
791        peer_addr: SocketAddr,
792        index: u32,
793        begin: u32,
794        length: u32,
795    },
796    AllowedFast {
797        peer_addr: SocketAddr,
798        index: u32,
799    },
800    SuggestPiece {
801        peer_addr: SocketAddr,
802        index: u32,
803    },
804    /// Peer successfully connected with a specific transport.
805    TransportIdentified {
806        peer_addr: SocketAddr,
807        transport: crate::rate_limiter::PeerTransport,
808    },
809    /// M140: BT handshake completed successfully — peer is now truly live.
810    /// Sent from `run_peer` after BT protocol handshake exchange validates
811    /// info_hash and peer_id. Triggers `mark_live()` in the actor.
812    HandshakeComplete {
813        peer_addr: SocketAddr,
814    },
815    Disconnected {
816        peer_addr: SocketAddr,
817        reason: Option<String>,
818    },
819    WebSeedPieceData {
820        url: String,
821        index: u32,
822        data: Bytes,
823    },
824    WebSeedError {
825        url: String,
826        piece: u32,
827        message: String,
828    },
829    /// BEP 52: Received hash response from peer.
830    HashesReceived {
831        peer_addr: SocketAddr,
832        request: irontide_core::HashRequest,
833        hashes: Vec<irontide_core::Id32>,
834    },
835    /// BEP 52: Peer rejected our hash request.
836    HashRequestRejected {
837        peer_addr: SocketAddr,
838        request: irontide_core::HashRequest,
839    },
840    /// BEP 52: Peer sent a hash request to us.
841    IncomingHashRequest {
842        peer_addr: SocketAddr,
843        request: irontide_core::HashRequest,
844    },
845    /// BEP 55: Received a Rendezvous request (we are the relay).
846    HolepunchRendezvous {
847        peer_addr: SocketAddr,
848        target: SocketAddr,
849    },
850    /// BEP 55: Received a Connect message (we should initiate simultaneous connect).
851    HolepunchConnect {
852        peer_addr: SocketAddr,
853        target: SocketAddr,
854    },
855    /// BEP 55: Received an Error message from the relay.
856    HolepunchError {
857        peer_addr: SocketAddr,
858        target: SocketAddr,
859        error_code: u32,
860    },
861    /// MSE handshake failed — peer is being retried with a different encryption mode.
862    /// Carries the new command channel sender so the TorrentActor can
863    /// update its PeerState.
864    MseRetry {
865        peer_addr: SocketAddr,
866        cmd_tx: tokio::sync::mpsc::Sender<PeerCommand>,
867    },
868    /// Peer released a piece it was downloading (choke, error, disconnect).
869    PieceReleased {
870        peer_addr: SocketAddr,
871        piece: u32,
872    },
873}
874
875/// Commands sent from the `TorrentActor` to a `PeerTask`.
876#[derive(Debug)]
877#[allow(dead_code)] // consumed by peer/torrent modules (not yet implemented)
878pub(crate) enum PeerCommand {
879    Request {
880        index: u32,
881        begin: u32,
882        length: u32,
883    },
884    Cancel {
885        index: u32,
886        begin: u32,
887        length: u32,
888    },
889    SetChoking(bool),
890    SetInterested(bool),
891    Have(u32),
892    RequestMetadata {
893        piece: u32,
894    },
895    RejectRequest {
896        index: u32,
897        begin: u32,
898        length: u32,
899    },
900    AllowedFast(u32),
901    SendPiece {
902        index: u32,
903        begin: u32,
904        data: Bytes,
905    },
906    /// Send an updated extension handshake (e.g. BEP 21 upload-only).
907    SendExtHandshake(irontide_wire::ExtHandshake),
908    /// BEP 6: Suggest a piece to the peer.
909    SuggestPiece(u32),
910    /// BEP 52: Send a hash request to the peer.
911    SendHashRequest(irontide_core::HashRequest),
912    /// BEP 52: Send hashes in response to a peer's request.
913    SendHashes {
914        request: irontide_core::HashRequest,
915        hashes: Vec<irontide_core::Id32>,
916    },
917    /// BEP 52: Reject a peer's hash request.
918    SendHashReject(irontide_core::HashRequest),
919    /// BEP 11: Send a PEX message to this peer.
920    SendPex {
921        message: crate::pex::PexMessage,
922    },
923    /// BEP 55: Send a holepunch message to this peer.
924    SendHolepunch(irontide_wire::HolepunchMessage),
925    /// Update the piece count after BEP 9 metadata assembly.
926    UpdateNumPieces(u32),
927    /// M159: Tell the peer task to stop dispatching block requests.
928    ///
929    /// Translated to `DispatchCommand::Stop` by the reader loop. The requester
930    /// transitions back to the idle state waiting for a fresh `StartRequesting`.
931    /// Uploads are unaffected.
932    StopRequesting,
933    /// M75: Actor sends reservation state to peer task for integrated dispatch.
934    /// Sent after metadata download (magnet) or at peer connection (non-magnet).
935    StartRequesting {
936        atomic_states: std::sync::Arc<crate::piece_reservation::AtomicPieceStates>,
937        availability_snapshot: std::sync::Arc<crate::piece_reservation::AvailabilitySnapshot>,
938        piece_notify: std::sync::Arc<tokio::sync::Notify>,
939        disk_handle: Option<crate::disk::DiskHandle>,
940        write_error_tx: tokio::sync::mpsc::Sender<crate::disk::DiskWriteError>,
941        /// Piece/chunk arithmetic for dispatch state initialization.
942        lengths: irontide_core::Lengths,
943        /// M103: Shared block-level request/received bitmaps for per-block stealing.
944        block_maps: Option<std::sync::Arc<crate::piece_reservation::BlockMaps>>,
945        /// M103: Shared queue of pieces available for block stealing.
946        steal_candidates: Option<std::sync::Arc<crate::piece_reservation::StealCandidates>>,
947        /// M120: Per-piece write guards to prevent steal/write races.
948        piece_write_guards: Option<std::sync::Arc<crate::piece_reservation::PieceWriteGuards>>,
949    },
950    /// Actor sends an updated availability snapshot to the peer task.
951    SnapshotUpdate {
952        snapshot: std::sync::Arc<crate::piece_reservation::AvailabilitySnapshot>,
953    },
954    Shutdown,
955}
956
957/// Helper trait combining [`AsyncRead`] + [`AsyncWrite`] for trait-object erasure.
958///
959/// Rust doesn't allow `dyn AsyncRead + AsyncWrite` directly, so this trait
960/// combines both into a single trait that can be used as a trait object.
961pub(crate) trait AsyncReadWrite:
962    tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send
963{
964}
965
966impl<T> AsyncReadWrite for T where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send {}
967
968/// Boxed async stream (AsyncRead + AsyncWrite + Unpin + Send) with a Debug impl.
969///
970/// Used for incoming SSL peer connections where the concrete TLS type is erased.
971pub(crate) struct BoxedAsyncStream(pub Box<dyn AsyncReadWrite>);
972
973impl std::fmt::Debug for BoxedAsyncStream {
974    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
975        f.write_str("BoxedAsyncStream(..)")
976    }
977}
978
979/// Commands sent from a `TorrentHandle` to the `TorrentActor`.
980#[derive(Debug)]
981#[allow(dead_code)] // consumed by torrent module (not yet implemented)
982pub(crate) enum TorrentCommand {
983    AddPeers {
984        peers: Vec<SocketAddr>,
985        source: crate::peer_state::PeerSource,
986    },
987    Stats {
988        reply: oneshot::Sender<TorrentStats>,
989    },
990    Pause,
991    Resume,
992    Shutdown,
993    SaveResumeData {
994        reply: oneshot::Sender<crate::Result<irontide_core::FastResumeData>>,
995    },
996    SetFilePriority {
997        index: usize,
998        priority: irontide_core::FilePriority,
999        reply: oneshot::Sender<crate::Result<()>>,
1000    },
1001    FilePriorities {
1002        reply: oneshot::Sender<Vec<irontide_core::FilePriority>>,
1003    },
1004    ForceReannounce,
1005    TrackerList {
1006        reply: oneshot::Sender<Vec<crate::tracker_manager::TrackerInfo>>,
1007    },
1008    Scrape {
1009        reply: oneshot::Sender<Option<(String, irontide_tracker::ScrapeInfo)>>,
1010    },
1011    /// Incoming peer routed from the session-level accept loop (TCP or uTP).
1012    IncomingPeer {
1013        stream: crate::transport::BoxedStream,
1014        addr: SocketAddr,
1015    },
1016    /// Open a streaming reader for a file within the torrent.
1017    OpenFile {
1018        file_index: usize,
1019        reply: oneshot::Sender<crate::Result<crate::streaming::FileStreamHandle>>,
1020    },
1021    /// Update the external IP for BEP 40 peer priority calculation.
1022    UpdateExternalIp {
1023        ip: std::net::IpAddr,
1024    },
1025    /// Move torrent data files to a new directory.
1026    MoveStorage {
1027        new_path: PathBuf,
1028        reply: oneshot::Sender<crate::Result<()>>,
1029    },
1030    /// Incoming SSL peer routed from the session-level SSL listener (M42).
1031    ///
1032    /// The TLS handshake has already been completed by the session actor.
1033    SpawnSslPeer {
1034        addr: SocketAddr,
1035        stream: BoxedAsyncStream,
1036    },
1037    /// Set the per-torrent download rate limit (bytes/sec, 0 = unlimited).
1038    SetDownloadLimit {
1039        bytes_per_sec: u64,
1040        reply: oneshot::Sender<()>,
1041    },
1042    /// Set the per-torrent upload rate limit (bytes/sec, 0 = unlimited).
1043    SetUploadLimit {
1044        bytes_per_sec: u64,
1045        reply: oneshot::Sender<()>,
1046    },
1047    /// Get the current per-torrent download rate limit (bytes/sec, 0 = unlimited).
1048    DownloadLimit {
1049        reply: oneshot::Sender<u64>,
1050    },
1051    /// Get the current per-torrent upload rate limit (bytes/sec, 0 = unlimited).
1052    UploadLimit {
1053        reply: oneshot::Sender<u64>,
1054    },
1055    /// Enable or disable sequential (in-order) piece downloading.
1056    SetSequentialDownload {
1057        enabled: bool,
1058        reply: oneshot::Sender<()>,
1059    },
1060    /// Query whether sequential downloading is enabled.
1061    IsSequentialDownload {
1062        reply: oneshot::Sender<bool>,
1063    },
1064    /// Enable or disable BEP 16 super seeding mode.
1065    SetSuperSeeding {
1066        enabled: bool,
1067        reply: oneshot::Sender<()>,
1068    },
1069    /// Query whether super seeding mode is enabled.
1070    IsSuperSeeding {
1071        reply: oneshot::Sender<bool>,
1072    },
1073    /// Enable or disable user-requested seed-only mode (M159).
1074    ///
1075    /// When enabled, the torrent stops scheduling new block requests and
1076    /// cancels all in-flight requests, but continues to serve uploads to
1077    /// interested peers. Mirrors libtorrent's `seed_mode` flag.
1078    SetSeedMode {
1079        enabled: bool,
1080        reply: oneshot::Sender<()>,
1081    },
1082    /// Add a new tracker URL (fire-and-forget at torrent level).
1083    AddTracker {
1084        url: String,
1085    },
1086    /// Replace all tracker URLs with a new set.
1087    ReplaceTrackers {
1088        urls: Vec<String>,
1089        reply: oneshot::Sender<()>,
1090    },
1091    /// Trigger a full piece verification (force recheck).
1092    ForceRecheck {
1093        reply: oneshot::Sender<crate::Result<()>>,
1094    },
1095    /// Rename a file within the torrent on disk.
1096    RenameFile {
1097        file_index: usize,
1098        new_name: String,
1099        reply: oneshot::Sender<crate::Result<()>>,
1100    },
1101    /// Set the per-torrent maximum number of connections (0 = use global default).
1102    SetMaxConnections {
1103        limit: usize,
1104        reply: oneshot::Sender<()>,
1105    },
1106    /// Get the current per-torrent maximum connection limit.
1107    MaxConnections {
1108        reply: oneshot::Sender<usize>,
1109    },
1110    /// Set the per-torrent maximum number of unchoke slots (upload slots).
1111    SetMaxUploads {
1112        limit: usize,
1113        reply: oneshot::Sender<()>,
1114    },
1115    /// Get the current per-torrent maximum unchoke slots (upload slots).
1116    MaxUploads {
1117        reply: oneshot::Sender<usize>,
1118    },
1119    /// Get per-peer details for all connected peers.
1120    GetPeerInfo {
1121        reply: oneshot::Sender<Vec<PeerInfo>>,
1122    },
1123    /// Get in-flight piece download status (the download queue).
1124    GetDownloadQueue {
1125        reply: oneshot::Sender<Vec<PartialPieceInfo>>,
1126    },
1127    /// Check whether a specific piece has been downloaded.
1128    HavePiece {
1129        index: u32,
1130        reply: oneshot::Sender<bool>,
1131    },
1132    /// Get per-piece availability counts from connected peers.
1133    PieceAvailability {
1134        reply: oneshot::Sender<Vec<u32>>,
1135    },
1136    /// Get per-file bytes-downloaded progress.
1137    FileProgress {
1138        reply: oneshot::Sender<Vec<u64>>,
1139    },
1140    /// Get the torrent's identity hashes (v1 and/or v2).
1141    InfoHashes {
1142        reply: oneshot::Sender<irontide_core::InfoHashes>,
1143    },
1144    /// Get the full v1 metainfo (None for magnet links before metadata received).
1145    TorrentFile {
1146        reply: oneshot::Sender<Option<irontide_core::TorrentMetaV1>>,
1147    },
1148    /// Get the full v2 metainfo (None if not a v2/hybrid torrent or before metadata received).
1149    TorrentFileV2 {
1150        reply: oneshot::Sender<Option<irontide_core::TorrentMetaV2>>,
1151    },
1152    /// Force an immediate DHT announce (fire-and-forget at torrent level).
1153    ForceDhtAnnounce,
1154    /// Read all data for a specific piece from disk.
1155    ReadPiece {
1156        index: u32,
1157        reply: oneshot::Sender<crate::Result<bytes::Bytes>>,
1158    },
1159    /// Flush the disk write cache for this torrent.
1160    FlushCache {
1161        reply: oneshot::Sender<crate::Result<()>>,
1162    },
1163    /// Clear the error state and resume if the torrent was paused due to error.
1164    ClearError,
1165    /// Get per-file open/mode status based on torrent state.
1166    FileStatus {
1167        reply: oneshot::Sender<Vec<crate::types::FileStatus>>,
1168    },
1169    /// Read the current torrent flags as a bitflag set.
1170    Flags {
1171        reply: oneshot::Sender<TorrentFlags>,
1172    },
1173    /// Set (enable) the specified torrent flags.
1174    SetFlags {
1175        flags: TorrentFlags,
1176        reply: oneshot::Sender<()>,
1177    },
1178    /// Unset (disable) the specified torrent flags.
1179    UnsetFlags {
1180        flags: TorrentFlags,
1181        reply: oneshot::Sender<()>,
1182    },
1183    /// Immediately initiate a peer connection to the given address.
1184    ConnectPeer {
1185        addr: SocketAddr,
1186    },
1187    /// Clear the `need_save_resume` dirty flag after a successful file save (M161).
1188    ClearSaveResumeFlag,
1189    /// Restore a piece bitmap from resume data (M161 Phase 4).
1190    ///
1191    /// Replaces the chunk tracker's bitfield with the provided raw piece bytes.
1192    /// The handler validates the bitfield length before applying.
1193    RestoreResumeBitmap {
1194        /// Raw piece bitfield bytes from resume data.
1195        pieces: Vec<u8>,
1196        /// Reply with `Ok(())` on success or an error if validation fails.
1197        reply: oneshot::Sender<crate::Result<()>>,
1198    },
1199    /// M147: Pre-resolved metadata from the background MetadataResolver.
1200    ///
1201    /// Sent by `SessionActor::spawn_metadata_resolver()` when the background
1202    /// resolver successfully obtains torrent metadata before the TorrentActor's
1203    /// own FetchingMetadata phase completes. This is a race: first to resolve
1204    /// wins; the other path's result is silently discarded.
1205    PreResolvedMetadata {
1206        /// Raw bencoded info dictionary bytes.
1207        info_bytes: Vec<u8>,
1208        /// Peers that were successfully connected during metadata resolution
1209        /// (for pre-seeding the peer pipeline).
1210        peers: Vec<SocketAddr>,
1211    },
1212}
1213
1214/// Per-peer details exported for client UI introspection.
1215#[derive(Debug, Clone, Serialize)]
1216pub struct PeerInfo {
1217    /// Remote peer address (IP + port).
1218    pub addr: SocketAddr,
1219    /// Client identification string (from extension handshake `v` field, or empty).
1220    pub client: String,
1221    /// Whether the peer is choking us.
1222    pub peer_choking: bool,
1223    /// Whether the peer is interested in our data.
1224    pub peer_interested: bool,
1225    /// Whether we are choking the peer.
1226    pub am_choking: bool,
1227    /// Whether we are interested in the peer's data.
1228    pub am_interested: bool,
1229    /// Current download rate from this peer in bytes/sec.
1230    pub download_rate: u64,
1231    /// Current upload rate to this peer in bytes/sec.
1232    pub upload_rate: u64,
1233    /// Number of pieces the peer has (bitfield population count).
1234    pub num_pieces: u32,
1235    /// How the peer was discovered.
1236    pub source: crate::peer_state::PeerSource,
1237    /// Whether the peer supports BEP 6 Fast Extension.
1238    pub supports_fast: bool,
1239    /// Whether the peer declared upload-only status (BEP 21).
1240    pub upload_only: bool,
1241    /// Whether the peer is snubbed (no data for snub_timeout_secs).
1242    pub snubbed: bool,
1243    /// Seconds since the peer connection was established.
1244    pub connected_duration_secs: u64,
1245    /// Number of outstanding piece requests to this peer.
1246    pub num_pending_requests: usize,
1247    /// Number of incoming piece requests from this peer.
1248    pub num_incoming_requests: usize,
1249}
1250
1251/// In-flight piece download status for the download queue.
1252#[derive(Debug, Clone, Serialize)]
1253pub struct PartialPieceInfo {
1254    /// Index of the piece being downloaded.
1255    pub piece_index: u32,
1256    /// Total number of blocks in this piece.
1257    pub blocks_in_piece: u32,
1258    /// Number of blocks that have been assigned to peers.
1259    pub blocks_assigned: u32,
1260}
1261
1262/// Info about a file within a torrent.
1263#[derive(Debug, Clone, Serialize)]
1264pub struct FileInfo {
1265    /// Relative path of the file within the torrent.
1266    pub path: PathBuf,
1267    /// File size in bytes.
1268    pub length: u64,
1269}
1270
1271/// Metadata about a torrent (available after metadata is fetched).
1272#[derive(Debug, Clone, Serialize)]
1273pub struct TorrentInfo {
1274    /// SHA-1 info hash of the torrent.
1275    pub info_hash: irontide_core::Id20,
1276    /// Display name from the torrent metadata.
1277    pub name: String,
1278    /// Total size of all files in bytes.
1279    pub total_length: u64,
1280    /// Size of each piece in bytes (last piece may be smaller).
1281    pub piece_length: u64,
1282    /// Total number of pieces in the torrent.
1283    pub num_pieces: u32,
1284    /// List of files contained in the torrent.
1285    pub files: Vec<FileInfo>,
1286    /// Whether this is a private torrent (DHT/PEX disabled).
1287    pub private: bool,
1288}
1289
1290/// Aggregate statistics for the whole session.
1291#[derive(Debug, Clone, Serialize, Deserialize)]
1292pub struct SessionStats {
1293    /// Number of non-paused torrents in the session.
1294    pub active_torrents: usize,
1295    /// Total bytes downloaded across all torrents since session start.
1296    pub total_downloaded: u64,
1297    /// Total bytes uploaded across all torrents since session start.
1298    pub total_uploaded: u64,
1299    /// Number of nodes in the DHT routing table.
1300    pub dht_nodes: usize,
1301}
1302
1303/// Whether a file in a torrent is open and its I/O access mode.
1304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1305pub enum FileMode {
1306    /// File is open for reading only (e.g. seeding).
1307    ReadOnly,
1308    /// File is open for reading and writing (e.g. downloading).
1309    ReadWrite,
1310    /// File is not currently open.
1311    Closed,
1312}
1313
1314/// Status of a single file within a torrent.
1315#[derive(Debug, Clone, Serialize)]
1316pub struct FileStatus {
1317    /// Whether the file is currently open.
1318    pub open: bool,
1319    /// The current access mode.
1320    pub mode: FileMode,
1321}
1322
1323bitflags! {
1324    /// Bitflag convenience wrapper for common torrent state flags.
1325    ///
1326    /// These map to existing torrent actor fields; `set_flags` / `unset_flags`
1327    /// delegate to the underlying operations (pause/resume, set_sequential, etc.).
1328    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1329    pub struct TorrentFlags: u32 {
1330        /// Torrent is paused.
1331        const PAUSED = 0x1;
1332        /// Torrent is auto-managed by the session queuing system.
1333        const AUTO_MANAGED = 0x2;
1334        /// Sequential (in-order) piece downloading is enabled.
1335        const SEQUENTIAL_DOWNLOAD = 0x4;
1336        /// BEP 16 super seeding mode is active.
1337        const SUPER_SEEDING = 0x8;
1338        /// Upload-only status (seeding complete, no wanted pieces).
1339        const UPLOAD_ONLY = 0x10;
1340    }
1341}
1342
1343/// Type alias for a factory that creates per-torrent storage.
1344pub type StorageFactory = Box<
1345    dyn Fn(
1346            &irontide_core::TorrentMetaV1,
1347            &std::path::Path,
1348        ) -> std::sync::Arc<dyn irontide_storage::TorrentStorage>
1349        + Send
1350        + Sync,
1351>;
1352
1353#[cfg(test)]
1354mod tests {
1355    use super::*;
1356
1357    #[test]
1358    fn torrent_config_strict_end_game_default() {
1359        let config = TorrentConfig::default();
1360        assert!(config.strict_end_game);
1361    }
1362
1363    #[test]
1364    fn torrent_config_bandwidth_defaults() {
1365        let config = TorrentConfig::default();
1366        assert_eq!(config.upload_rate_limit, 0);
1367        assert_eq!(config.download_rate_limit, 0);
1368    }
1369
1370    #[test]
1371    fn torrent_config_encryption_default() {
1372        let cfg = TorrentConfig::default();
1373        assert_eq!(
1374            cfg.encryption_mode,
1375            irontide_wire::mse::EncryptionMode::Disabled
1376        );
1377    }
1378
1379    #[test]
1380    fn torrent_config_utp_default() {
1381        let cfg = TorrentConfig::default();
1382        assert!(cfg.enable_utp);
1383    }
1384
1385    #[test]
1386    fn torrent_config_web_seed_defaults() {
1387        let cfg = TorrentConfig::default();
1388        assert!(cfg.enable_web_seed);
1389        assert_eq!(cfg.max_web_seeds, 4);
1390    }
1391
1392    #[test]
1393    fn torrent_config_super_seeding_default() {
1394        let cfg = TorrentConfig::default();
1395        assert!(!cfg.super_seeding);
1396        assert!(cfg.upload_only_announce);
1397    }
1398
1399    #[test]
1400    fn torrent_config_picker_defaults() {
1401        let cfg = TorrentConfig::default();
1402        assert!(!cfg.sequential_download);
1403        assert_eq!(cfg.initial_picker_threshold, 4);
1404        assert_eq!(cfg.whole_pieces_threshold, 20);
1405        assert_eq!(cfg.snub_timeout_secs, 15);
1406        assert_eq!(cfg.readahead_pieces, 8);
1407        assert!(cfg.streaming_timeout_escalation);
1408    }
1409
1410    #[test]
1411    fn torrent_stats_has_peers_by_source() {
1412        use crate::peer_state::PeerSource;
1413        use std::collections::HashMap;
1414
1415        let stats = TorrentStats {
1416            state: TorrentState::Downloading,
1417            pieces_total: 10,
1418            ..Default::default()
1419        };
1420        assert!(stats.peers_by_source.is_empty());
1421
1422        let mut map = HashMap::new();
1423        map.insert(PeerSource::Tracker, 5);
1424        map.insert(PeerSource::Dht, 3);
1425        let stats2 = TorrentStats {
1426            peers_by_source: map.clone(),
1427            ..stats
1428        };
1429        assert_eq!(stats2.peers_by_source[&PeerSource::Tracker], 5);
1430        assert_eq!(stats2.peers_by_source[&PeerSource::Dht], 3);
1431    }
1432
1433    #[test]
1434    fn torrent_stats_default_values() {
1435        let stats = TorrentStats::default();
1436
1437        // State
1438        assert_eq!(stats.state, TorrentState::Paused);
1439
1440        // Original fields are zeroed
1441        assert_eq!(stats.downloaded, 0);
1442        assert_eq!(stats.uploaded, 0);
1443        assert_eq!(stats.pieces_have, 0);
1444        assert_eq!(stats.pieces_total, 0);
1445        assert_eq!(stats.peers_connected, 0);
1446        assert_eq!(stats.peers_available, 0);
1447        assert!((stats.checking_progress - 0.0).abs() < f32::EPSILON);
1448        assert!(stats.peers_by_source.is_empty());
1449
1450        // Identity: zeroed info hash
1451        assert_eq!(
1452            stats.info_hashes,
1453            irontide_core::InfoHashes::v1_only(irontide_core::Id20::from([0u8; 20]))
1454        );
1455        assert!(stats.name.is_empty());
1456
1457        // State flags are all false
1458        assert!(!stats.has_metadata);
1459        assert!(!stats.is_seeding);
1460        assert!(!stats.is_finished);
1461        assert!(!stats.is_paused);
1462        assert!(!stats.auto_managed);
1463        assert!(!stats.sequential_download);
1464        assert!(!stats.super_seeding);
1465        assert!(!stats.has_incoming);
1466        assert!(!stats.need_save_resume);
1467        assert!(!stats.moving_storage);
1468
1469        // Progress
1470        assert!((stats.progress - 0.0).abs() < f32::EPSILON);
1471        assert_eq!(stats.progress_ppm, 0);
1472        assert_eq!(stats.total_done, 0);
1473        assert_eq!(stats.total, 0);
1474        assert_eq!(stats.total_wanted_done, 0);
1475        assert_eq!(stats.total_wanted, 0);
1476        assert_eq!(stats.block_size, 16384);
1477
1478        // Sentinel values
1479        assert_eq!(stats.num_complete, -1);
1480        assert_eq!(stats.num_incomplete, -1);
1481        assert_eq!(stats.queue_position, -1);
1482        assert_eq!(stats.error_file, -1);
1483
1484        // Strings are empty
1485        assert!(stats.current_tracker.is_empty());
1486        assert!(stats.save_path.is_empty());
1487        assert!(stats.error.is_empty());
1488
1489        // Rates are zero
1490        assert_eq!(stats.download_rate, 0);
1491        assert_eq!(stats.upload_rate, 0);
1492        assert_eq!(stats.download_payload_rate, 0);
1493        assert_eq!(stats.upload_payload_rate, 0);
1494
1495        // Distributed copies
1496        assert_eq!(stats.distributed_full_copies, 0);
1497        assert_eq!(stats.distributed_fraction, 0);
1498        assert!((stats.distributed_copies - 0.0).abs() < f32::EPSILON);
1499    }
1500
1501    #[test]
1502    fn torrent_stats_seeding_flags() {
1503        let stats = TorrentStats {
1504            state: TorrentState::Seeding,
1505            is_seeding: true,
1506            is_finished: true,
1507            has_metadata: true,
1508            progress: 1.0,
1509            progress_ppm: 1_000_000,
1510            ..Default::default()
1511        };
1512        assert_eq!(stats.state, TorrentState::Seeding);
1513        assert!(stats.is_seeding);
1514        assert!(stats.is_finished);
1515        assert!(stats.has_metadata);
1516        assert!((stats.progress - 1.0).abs() < f32::EPSILON);
1517        assert_eq!(stats.progress_ppm, 1_000_000);
1518        // Other fields remain default
1519        assert!(!stats.is_paused);
1520        assert_eq!(stats.downloaded, 0);
1521    }
1522
1523    #[test]
1524    fn torrent_state_sharing_variant() {
1525        let state = TorrentState::Sharing;
1526        assert_ne!(state, TorrentState::Downloading);
1527        assert_ne!(state, TorrentState::Seeding);
1528        // Verify JSON round-trip
1529        let json = serde_json::to_string(&state).unwrap();
1530        assert_eq!(json, "\"Sharing\"");
1531        let decoded: TorrentState = serde_json::from_str(&json).unwrap();
1532        assert_eq!(decoded, TorrentState::Sharing);
1533    }
1534
1535    #[test]
1536    fn torrent_config_i2p_defaults() {
1537        let cfg = TorrentConfig::default();
1538        assert!(!cfg.enable_i2p);
1539        assert!(!cfg.allow_i2p_mixed);
1540    }
1541
1542    #[test]
1543    fn torrent_config_ssl_listen_port_default() {
1544        let cfg = TorrentConfig::default();
1545        assert_eq!(cfg.ssl_listen_port, 0);
1546    }
1547
1548    #[test]
1549    fn torrent_config_ssl_listen_port_from_settings() {
1550        let mut s = crate::settings::Settings::default();
1551        s.ssl_listen_port = 4433;
1552        let tc = TorrentConfig::from(&s);
1553        assert_eq!(tc.ssl_listen_port, 4433);
1554    }
1555
1556    #[test]
1557    fn torrent_config_choking_defaults() {
1558        let cfg = TorrentConfig::default();
1559        assert_eq!(
1560            cfg.seed_choking_algorithm,
1561            SeedChokingAlgorithm::FastestUpload
1562        );
1563        assert_eq!(cfg.choking_algorithm, ChokingAlgorithm::FixedSlots);
1564    }
1565
1566    #[test]
1567    fn torrent_config_m44_defaults() {
1568        let cfg = TorrentConfig::default();
1569        assert!(cfg.piece_extent_affinity);
1570        assert!(!cfg.suggest_mode);
1571        assert_eq!(cfg.max_suggest_pieces, 10);
1572        assert_eq!(cfg.predictive_piece_announce_ms, 0);
1573    }
1574
1575    #[test]
1576    fn torrent_config_from_settings_choking() {
1577        let mut s = crate::settings::Settings::default();
1578        s.seed_choking_algorithm = SeedChokingAlgorithm::RoundRobin;
1579        s.choking_algorithm = ChokingAlgorithm::RateBased;
1580        let cfg = TorrentConfig::from(&s);
1581        assert_eq!(cfg.seed_choking_algorithm, SeedChokingAlgorithm::RoundRobin);
1582        assert_eq!(cfg.choking_algorithm, ChokingAlgorithm::RateBased);
1583    }
1584
1585    #[test]
1586    fn torrent_config_holepunch_default() {
1587        let cfg = TorrentConfig::default();
1588        assert!(cfg.enable_holepunch);
1589
1590        // Also verify it inherits from Settings
1591        let s = crate::settings::Settings::default();
1592        let tc = TorrentConfig::from(&s);
1593        assert!(tc.enable_holepunch);
1594
1595        // And when disabled in Settings
1596        let mut s2 = crate::settings::Settings::default();
1597        s2.enable_holepunch = false;
1598        let tc2 = TorrentConfig::from(&s2);
1599        assert!(!tc2.enable_holepunch);
1600    }
1601
1602    #[test]
1603    fn torrent_config_url_security_default() {
1604        let cfg = TorrentConfig::default();
1605        assert!(cfg.url_security.ssrf_mitigation);
1606        assert!(!cfg.url_security.allow_idna);
1607        assert!(cfg.url_security.validate_https_trackers);
1608    }
1609
1610    #[test]
1611    fn torrent_config_url_security_from_settings() {
1612        let mut s = crate::settings::Settings::default();
1613        s.ssrf_mitigation = false;
1614        s.allow_idna = true;
1615        s.validate_https_trackers = false;
1616        let cfg = TorrentConfig::from(&s);
1617        assert!(!cfg.url_security.ssrf_mitigation);
1618        assert!(cfg.url_security.allow_idna);
1619        assert!(!cfg.url_security.validate_https_trackers);
1620    }
1621
1622    #[test]
1623    fn torrent_config_peer_dscp_default() {
1624        let cfg = TorrentConfig::default();
1625        assert_eq!(cfg.peer_dscp, 0x08);
1626    }
1627
1628    #[test]
1629    fn torrent_config_peer_dscp_from_settings() {
1630        let mut s = crate::settings::Settings::default();
1631        s.peer_dscp = 0x2E;
1632        let cfg = TorrentConfig::from(&s);
1633        assert_eq!(cfg.peer_dscp, 0x2E);
1634    }
1635
1636    // ── M121: TorrentSummary and Serialize tests ──
1637
1638    #[test]
1639    fn summary_from_stats() {
1640        let v1_hash =
1641            irontide_core::Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
1642        let mut stats = TorrentStats::default();
1643        stats.info_hashes = irontide_core::InfoHashes::v1_only(v1_hash);
1644        stats.name = "test torrent".to_string();
1645        stats.state = TorrentState::Downloading;
1646        stats.progress = 0.75;
1647        stats.download_rate = 1_000_000;
1648        stats.upload_rate = 500_000;
1649        stats.total = 100_000_000;
1650        stats.num_peers = 42;
1651        stats.added_time = 1710900000;
1652
1653        let summary = super::TorrentSummary::from(&stats);
1654        assert_eq!(
1655            summary.info_hash,
1656            "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
1657        );
1658        assert_eq!(summary.name, "test torrent");
1659        assert_eq!(summary.state, TorrentState::Downloading);
1660        assert!((summary.progress - 0.75).abs() < f64::EPSILON);
1661        assert_eq!(summary.download_rate, 1_000_000);
1662        assert_eq!(summary.upload_rate, 500_000);
1663        assert_eq!(summary.total_size, 100_000_000);
1664        assert_eq!(summary.num_peers, 42);
1665        assert_eq!(summary.added_time, 1710900000);
1666    }
1667
1668    #[test]
1669    fn summary_from_stats_v2_only() {
1670        let v2_hash = irontide_core::Id32::from_hex(
1671            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1672        )
1673        .unwrap();
1674        let mut stats = TorrentStats::default();
1675        stats.info_hashes = irontide_core::InfoHashes::v2_only(v2_hash);
1676        stats.name = "v2 torrent".to_string();
1677
1678        let summary = super::TorrentSummary::from(&stats);
1679        // v2-only torrents have no v1 hash, so info_hash is empty
1680        assert_eq!(summary.info_hash, "");
1681        assert_eq!(summary.name, "v2 torrent");
1682    }
1683
1684    #[test]
1685    fn stats_serializable() {
1686        let stats = TorrentStats::default();
1687        let json = serde_json::to_string(&stats).expect("TorrentStats should serialize to JSON");
1688        assert!(json.contains("\"state\""));
1689        assert!(json.contains("\"info_hashes\""));
1690        assert!(json.contains("\"download_rate\""));
1691    }
1692
1693    #[test]
1694    fn info_hashes_serializable() {
1695        let v1 = irontide_core::Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
1696        let ih = irontide_core::InfoHashes::v1_only(v1);
1697        let json = serde_json::to_string(&ih).expect("InfoHashes should serialize to JSON");
1698        // Id20 serializes as raw bytes (not hex) — verify JSON structure
1699        assert!(json.contains("\"v1\""));
1700        assert!(json.contains("\"v2\":null"));
1701    }
1702
1703    #[test]
1704    fn summary_serializable() {
1705        let mut stats = TorrentStats::default();
1706        stats.name = "serialize test".to_string();
1707        stats.state = TorrentState::Seeding;
1708        stats.progress = 1.0;
1709        let summary = super::TorrentSummary::from(&stats);
1710        let json =
1711            serde_json::to_string(&summary).expect("TorrentSummary should serialize to JSON");
1712        assert!(json.contains("\"name\":\"serialize test\""));
1713        assert!(json.contains("\"state\":\"Seeding\""));
1714        assert!(json.contains("\"progress\":1.0"));
1715    }
1716
1717    #[test]
1718    fn test_torrent_summary_includes_seeds_and_totals() {
1719        let mut stats = TorrentStats::default();
1720        stats.num_seeds = 7;
1721        stats.all_time_upload = 1_500_000;
1722        stats.all_time_download = 3_000_000;
1723
1724        let summary = super::TorrentSummary::from(&stats);
1725        assert_eq!(summary.num_seeds, 7);
1726        assert_eq!(summary.all_time_upload, 1_500_000);
1727        assert_eq!(summary.all_time_download, 3_000_000);
1728    }
1729}