Skip to main content

irontide_session/
settings.rs

1//! Unified settings pack for session configuration.
2//!
3//! Replaces the old `SessionConfig` with a single strongly-typed struct that
4//! consolidates all configurable knobs. Supports presets, validation, and
5//! serde serialization (bencode + JSON).
6
7use std::net::IpAddr;
8use std::path::PathBuf;
9
10use serde::{Deserialize, Serialize};
11
12use irontide_core::StorageMode;
13use irontide_wire::mse::EncryptionMode;
14
15use crate::alert::AlertCategory;
16use crate::choker::{ChokingAlgorithm, SeedChokingAlgorithm};
17use crate::proxy::ProxyConfig;
18use crate::rate_limiter::MixedModeAlgorithm;
19
20// ── Serde default helpers ────────────────────────────────────────────
21
22fn default_true() -> bool {
23    true
24}
25fn default_listen_port() -> u16 {
26    42020
27}
28fn default_download_dir() -> PathBuf {
29    PathBuf::from(".")
30}
31fn default_max_torrents() -> usize {
32    100
33}
34fn default_encryption() -> EncryptionMode {
35    EncryptionMode::Disabled
36}
37fn default_auto_upload_slots_min() -> usize {
38    2
39}
40fn default_auto_upload_slots_max() -> usize {
41    20
42}
43fn default_active_downloads() -> i32 {
44    3
45}
46fn default_active_seeds() -> i32 {
47    5
48}
49fn default_active_limit() -> i32 {
50    500
51}
52fn default_active_checking() -> i32 {
53    1
54}
55fn default_inactive_rate() -> u64 {
56    2048
57}
58fn default_auto_manage_interval() -> u64 {
59    30
60}
61fn default_auto_manage_startup() -> u64 {
62    60
63}
64fn default_alert_mask() -> AlertCategory {
65    AlertCategory::ALL
66}
67fn default_alert_channel_size() -> usize {
68    1024
69}
70fn default_smart_ban_max_failures() -> u32 {
71    3
72}
73fn default_disk_io_threads() -> usize {
74    let cores = std::thread::available_parallelism()
75        .map(|n| n.get())
76        .unwrap_or(4);
77    (cores / 2).clamp(4, 16)
78}
79fn default_max_blocking_threads() -> usize {
80    std::thread::available_parallelism()
81        .map(|n| n.get())
82        .unwrap_or(4)
83}
84fn default_storage_mode() -> StorageMode {
85    StorageMode::Auto
86}
87fn default_disk_cache_size() -> usize {
88    16 * 1024 * 1024
89}
90fn default_disk_write_cache_ratio() -> f32 {
91    0.5
92}
93fn default_buffer_pool_capacity() -> usize {
94    64 * 1024 * 1024
95}
96fn default_enable_mlock() -> bool {
97    cfg!(unix)
98}
99fn default_io_uring_sq_depth() -> u32 {
100    256
101}
102fn default_io_uring_batch_threshold() -> usize {
103    4
104}
105fn default_disk_channel_capacity() -> usize {
106    512
107}
108fn default_hashing_threads() -> usize {
109    let cores = std::thread::available_parallelism()
110        .map(|n| n.get())
111        .unwrap_or(4);
112    (cores / 4).clamp(2, 8)
113}
114fn default_max_request_queue_depth() -> usize {
115    250
116}
117fn default_initial_queue_depth() -> usize {
118    128
119}
120fn default_request_queue_time() -> f64 {
121    3.0
122}
123fn default_block_request_timeout() -> u32 {
124    60
125}
126fn default_max_concurrent_streams() -> usize {
127    8
128}
129fn default_dht_qps() -> usize {
130    50
131}
132fn default_dht_timeout() -> u64 {
133    5
134}
135fn default_upnp_lease() -> u32 {
136    3600
137}
138fn default_natpmp_lifetime() -> u32 {
139    7200
140}
141fn default_utp_max_conns() -> usize {
142    256
143}
144fn default_dht_max_items() -> usize {
145    700
146}
147fn default_dht_item_lifetime() -> u64 {
148    7200
149}
150fn default_dht_sample_interval() -> u64 {
151    0
152}
153fn default_max_suggest_pieces() -> usize {
154    16
155}
156fn default_predictive_piece_announce_ms() -> u64 {
157    0
158}
159fn default_ssl_listen_port() -> u16 {
160    0 // 0 = disabled
161}
162fn default_seed_choking_algorithm() -> SeedChokingAlgorithm {
163    SeedChokingAlgorithm::FastestUpload
164}
165fn default_choking_algorithm() -> ChokingAlgorithm {
166    ChokingAlgorithm::FixedSlots
167}
168fn default_mixed_mode() -> MixedModeAlgorithm {
169    MixedModeAlgorithm::PeerProportional
170}
171fn default_steal_threshold_ratio() -> f64 {
172    10.0
173}
174fn default_use_block_stealing() -> bool {
175    true
176}
177fn default_peer_connect_timeout() -> u64 {
178    10 // M139: match rqbit — longer timeout produces more natural connect failures for cycling
179}
180fn default_peer_dscp() -> u8 {
181    0x08 // CS1 (scavenger/low-priority)
182}
183fn default_max_peers_per_torrent() -> usize {
184    128
185}
186fn default_stats_report_interval() -> u64 {
187    1000
188}
189fn default_strict_end_game() -> bool {
190    true
191}
192fn default_max_web_seeds() -> usize {
193    4
194}
195fn default_initial_picker_threshold() -> u32 {
196    4
197}
198fn default_whole_pieces_threshold() -> u32 {
199    20
200}
201fn default_snub_timeout_secs() -> u32 {
202    15
203}
204fn default_readahead_pieces() -> u32 {
205    8
206}
207fn default_max_metadata_size() -> u64 {
208    4 * 1024 * 1024 // 4 MiB — libtorrent default
209}
210fn default_max_message_size() -> usize {
211    16 * 1024 * 1024 // 16 MiB — matches wire codec constant
212}
213fn default_max_piece_length() -> u64 {
214    32 * 1024 * 1024 // 32 MiB — largest reasonable piece size
215}
216fn default_max_outstanding_requests() -> usize {
217    500
218}
219fn default_max_in_flight_pieces() -> usize {
220    512
221}
222fn default_fixed_pipeline_depth() -> usize {
223    128
224}
225fn default_i2p_hostname() -> String {
226    "127.0.0.1".into()
227}
228fn default_i2p_port() -> u16 {
229    7656
230}
231fn default_i2p_tunnel_quantity() -> u8 {
232    3
233}
234fn default_i2p_tunnel_length() -> u8 {
235    3
236}
237fn default_runtime_worker_threads() -> usize {
238    std::thread::available_parallelism()
239        .map(|n| n.get().min(8))
240        .unwrap_or(4)
241}
242fn default_lock_warn_threshold_ms() -> u64 {
243    50
244}
245fn default_steal_stale_piece_secs() -> u64 {
246    2
247}
248fn default_steal_threshold_endgame() -> f64 {
249    3.0
250}
251fn default_min_pipeline_depth() -> u32 {
252    16
253}
254fn default_max_pipeline_depth() -> u32 {
255    512
256}
257fn default_target_buffer_secs() -> f64 {
258    2.0
259}
260fn default_peer_read_timeout_secs() -> u64 {
261    10
262}
263fn default_peer_write_timeout_secs() -> u64 {
264    10
265}
266fn default_data_contribution_timeout() -> u64 {
267    0 // M139: disabled by default — rqbit doesn't evict for no data
268}
269fn default_choke_rotation_max_evictions() -> u32 {
270    0 // M139: disabled by default — rqbit doesn't proactively rotate choked peers
271}
272fn default_max_concurrent_connects() -> u16 {
273    128 // M147: ConnectPool — gates connection attempts, released on handshake
274}
275fn default_connect_soft_timeout() -> u64 {
276    3 // M147: seconds without TCP SYN-ACK before soft reap disconnects
277}
278fn default_save_resume_interval() -> u64 {
279    300 // M161: 5 minutes between periodic resume file saves
280}
281
282// ── Settings ─────────────────────────────────────────────────────────
283
284/// Unified session settings (replaces `SessionConfig`).
285///
286/// All 56 configurable fields in a single strongly-typed struct.
287/// Supports presets via factory functions and runtime mutation via
288/// `SessionHandle::apply_settings()`.
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct Settings {
291    // ── General ──
292    /// TCP listen port for incoming peer connections (default: 42020).
293    #[serde(default = "default_listen_port")]
294    pub listen_port: u16,
295    /// Default download directory for new torrents (default: ".").
296    #[serde(default = "default_download_dir")]
297    pub download_dir: PathBuf,
298    /// Maximum number of concurrent torrents (default: 100).
299    #[serde(default = "default_max_torrents")]
300    pub max_torrents: usize,
301    /// Directory for fast-resume data files. If `None`, resume data is not persisted.
302    #[serde(default, skip_serializing_if = "Option::is_none")]
303    pub resume_data_dir: Option<PathBuf>,
304    /// Interval in seconds between periodic resume file saves (0 = disabled).
305    /// Default: 300 (5 minutes).
306    #[serde(default = "default_save_resume_interval")]
307    pub save_resume_interval_secs: u64,
308
309    // ── Protocol features ──
310    /// Enable Kademlia DHT peer discovery (BEP 5). Default: true.
311    #[serde(default = "default_true")]
312    pub enable_dht: bool,
313    /// Enable Peer Exchange (BEP 11). Default: true.
314    #[serde(default = "default_true")]
315    pub enable_pex: bool,
316    /// Enable Local Service Discovery via multicast (BEP 14). Default: true.
317    #[serde(default = "default_true")]
318    pub enable_lsd: bool,
319    /// Enable BEP 6 Fast Extension (AllowedFast, HaveAll, HaveNone, Reject,
320    /// SuggestPiece). Default: true.
321    #[serde(default = "default_true")]
322    pub enable_fast_extension: bool,
323    /// Enable uTP (BEP 29) micro transport protocol. When enabled, outbound
324    /// connections try uTP first with a 5-second timeout before falling back
325    /// to TCP. Default: true.
326    #[serde(default = "default_true")]
327    pub enable_utp: bool,
328    /// Enable UPnP IGD port mapping (last resort after PCP and NAT-PMP).
329    /// Default: true.
330    #[serde(default = "default_true")]
331    pub enable_upnp: bool,
332    /// Enable NAT-PMP (RFC 6886) and PCP (RFC 6887) port mapping.
333    /// PCP is tried first, then NAT-PMP as fallback. Default: true.
334    #[serde(default = "default_true")]
335    pub enable_natpmp: bool,
336    /// Enable IPv6 dual-stack support (BEP 7, 24). Binds listeners on both
337    /// IPv4 and IPv6, starts a second DHT instance, and processes IPv6 peers
338    /// in PEX and tracker responses. Default: true.
339    #[serde(default = "default_true")]
340    pub enable_ipv6: bool,
341    /// Enable HTTP/web seeding (BEP 19 GetRight, BEP 17 Hoffman). Torrents
342    /// with `url-list` or `httpseeds` download pieces from HTTP servers
343    /// alongside peer-to-peer transfers. Default: true.
344    #[serde(default = "default_true")]
345    pub enable_web_seed: bool,
346    /// Enable BEP 55 holepunch extension for NAT traversal. Advertises
347    /// `ut_holepunch` in the extension handshake and can act as initiator,
348    /// relay, or target for holepunch connections. Default: true.
349    #[serde(default = "default_true")]
350    pub enable_holepunch: bool,
351    /// Enable BEP 40 canonical peer priority for connection eviction.
352    /// When at capacity, incoming peers with higher deterministic priority
353    /// can displace lower-priority ones. Default: true.
354    #[serde(default = "default_true")]
355    pub enable_bep40_eviction: bool,
356    /// Connection encryption mode (MSE/PE). Default: Disabled.
357    #[serde(default = "default_encryption")]
358    pub encryption_mode: EncryptionMode,
359    /// Suppress identifying information (client version in BEP 10 handshake)
360    /// and disable DHT, LSD, UPnP, and NAT-PMP. Default: false.
361    #[serde(default)]
362    pub anonymous_mode: bool,
363    /// Manually configured external IP for BEP 40 peer priority.
364    /// If not set, discovered automatically via NAT traversal.
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub external_ip: Option<IpAddr>,
367
368    // ── Seeding ──
369    /// Stop seeding when this upload/download ratio is reached. `None` = unlimited.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub seed_ratio_limit: Option<f64>,
372    /// Enable BEP 16 super seeding for new torrents. Reveals pieces one-per-peer
373    /// to maximize piece diversity across the swarm. Default: false.
374    #[serde(default)]
375    pub default_super_seeding: bool,
376    /// Default share mode for new torrents. When true, torrents relay pieces
377    /// in memory without writing to disk. Requires fast extension (BEP 6).
378    #[serde(default)]
379    pub default_share_mode: bool,
380    /// Advertise upload-only status via extension handshake when a torrent
381    /// transitions to seeding (BEP 21). Default: true.
382    #[serde(default = "default_true")]
383    pub upload_only_announce: bool,
384    // ── Rate limiting ──
385    /// Global upload rate limit in bytes/sec (0 = unlimited).
386    #[serde(default)]
387    pub upload_rate_limit: u64,
388    /// Global download rate limit in bytes/sec (0 = unlimited).
389    #[serde(default)]
390    pub download_rate_limit: u64,
391    /// TCP upload rate limit in bytes/sec (0 = unlimited).
392    #[serde(default)]
393    pub tcp_upload_rate_limit: u64,
394    /// TCP download rate limit in bytes/sec (0 = unlimited).
395    #[serde(default)]
396    pub tcp_download_rate_limit: u64,
397    /// uTP upload rate limit in bytes/sec (0 = unlimited).
398    #[serde(default)]
399    pub utp_upload_rate_limit: u64,
400    /// uTP download rate limit in bytes/sec (0 = unlimited).
401    #[serde(default)]
402    pub utp_download_rate_limit: u64,
403    /// Automatically adjust the number of upload slots based on bandwidth. Default: true.
404    #[serde(default = "default_true")]
405    pub auto_upload_slots: bool,
406    /// Minimum number of automatic upload slots (default: 2).
407    #[serde(default = "default_auto_upload_slots_min")]
408    pub auto_upload_slots_min: usize,
409    /// Maximum number of automatic upload slots (default: 20).
410    #[serde(default = "default_auto_upload_slots_max")]
411    pub auto_upload_slots_max: usize,
412    /// Mixed-mode TCP/uTP bandwidth allocation algorithm.
413    #[serde(default = "default_mixed_mode")]
414    pub mixed_mode_algorithm: MixedModeAlgorithm,
415
416    // ── Queue management ──
417    /// Maximum concurrent auto-managed downloading torrents (-1 = unlimited, default: 3).
418    #[serde(default = "default_active_downloads")]
419    pub active_downloads: i32,
420    /// Maximum concurrent auto-managed seeding torrents (-1 = unlimited, default: 5).
421    #[serde(default = "default_active_seeds")]
422    pub active_seeds: i32,
423    /// Hard cap on all active auto-managed torrents (-1 = unlimited, default: 500).
424    #[serde(default = "default_active_limit")]
425    pub active_limit: i32,
426    /// Maximum concurrent hash-check operations (default: 1).
427    #[serde(default = "default_active_checking")]
428    pub active_checking: i32,
429    /// Exempt inactive torrents from download/seed limits. A torrent is inactive
430    /// if its rate is below `inactive_down_rate` / `inactive_up_rate`. Default: true.
431    #[serde(default = "default_true")]
432    pub dont_count_slow_torrents: bool,
433    /// Download rate threshold (bytes/sec) below which a torrent is considered
434    /// inactive for queue management purposes (default: 2048).
435    #[serde(default = "default_inactive_rate")]
436    pub inactive_down_rate: u64,
437    /// Upload rate threshold (bytes/sec) below which a torrent is considered
438    /// inactive for queue management purposes (default: 2048).
439    #[serde(default = "default_inactive_rate")]
440    pub inactive_up_rate: u64,
441    /// Interval in seconds between queue evaluations (default: 30).
442    #[serde(default = "default_auto_manage_interval")]
443    pub auto_manage_interval: u64,
444    /// Grace period in seconds where a torrent is considered active regardless
445    /// of speed after being started (default: 60).
446    #[serde(default = "default_auto_manage_startup")]
447    pub auto_manage_startup: u64,
448    /// Allocate seeding slots before download slots. Default: false.
449    #[serde(default)]
450    pub auto_manage_prefer_seeds: bool,
451
452    // ── Alerts ──
453    /// Bitmask of alert categories to receive (default: ALL).
454    #[serde(default = "default_alert_mask")]
455    pub alert_mask: AlertCategory,
456    /// Capacity of the alert broadcast channel (default: 1024).
457    #[serde(default = "default_alert_channel_size")]
458    pub alert_channel_size: usize,
459
460    // ── Smart banning ──
461    /// Number of hash-failure involvements before a peer is auto-banned.
462    /// Lower values ban faster but risk false positives (default: 3).
463    #[serde(default = "default_smart_ban_max_failures")]
464    pub smart_ban_max_failures: u32,
465    /// Enable parole mode: re-download a failed piece from a single uninvolved
466    /// peer to definitively attribute fault before striking. Default: true.
467    #[serde(default = "default_true")]
468    pub smart_ban_parole: bool,
469
470    // ── Disk I/O ──
471    /// Number of concurrent disk I/O threads (default: 4).
472    #[serde(default = "default_disk_io_threads")]
473    pub disk_io_threads: usize,
474    /// Maximum number of concurrent blocking I/O operations dispatched via
475    /// `block_in_place`. Defaults to the number of available CPU cores.
476    #[serde(default = "default_max_blocking_threads")]
477    pub max_blocking_threads: usize,
478    /// Storage allocation mode: Auto, FullPreallocate, or SparseFile (default: Auto).
479    #[serde(default = "default_storage_mode")]
480    pub storage_mode: StorageMode,
481    /// Override pre-allocation strategy (None/Sparse/Full). When `None` (default),
482    /// derived from `storage_mode`: Full → PreallocateMode::Full, else → PreallocateMode::None.
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub preallocate_mode: Option<irontide_storage::PreallocateMode>,
485    /// Total ARC disk cache size in bytes (default: 16 MiB, minimum: 1 MiB).
486    #[serde(default = "default_disk_cache_size")]
487    pub disk_cache_size: usize,
488    /// Fraction of disk cache reserved for write buffering (0.0–1.0, default: 0.5).
489    #[serde(default = "default_disk_write_cache_ratio")]
490    pub disk_write_cache_ratio: f32,
491    /// Capacity of the async disk I/O command channel (default: 512).
492    #[serde(default = "default_disk_channel_capacity")]
493    pub disk_channel_capacity: usize,
494    /// Unified buffer pool capacity in bytes (default: 64 MiB).
495    /// Replaces disk_cache_size when set. Covers both write buffering and read cache.
496    #[serde(default = "default_buffer_pool_capacity")]
497    pub buffer_pool_capacity: usize,
498    /// Lock cached piece data in physical memory (default: true on Unix).
499    /// Prevents the OS from swapping out hot cache entries. Silently ignored
500    /// if RLIMIT_MEMLOCK is exceeded.
501    #[serde(default = "default_enable_mlock")]
502    pub enable_mlock: bool,
503    /// io_uring submission queue depth (number of SQEs). Only used when
504    /// `storage_mode` is `IoUring`. Default: 256.
505    #[serde(default = "default_io_uring_sq_depth")]
506    pub io_uring_sq_depth: u32,
507    /// Enable O_DIRECT for io_uring writes, bypassing the kernel page cache.
508    /// Unaligned writes fall back to regular pwritev. Default: false.
509    #[serde(default)]
510    pub io_uring_direct_io: bool,
511    /// Enable direct I/O for filesystem storage (bypasses kernel page cache).
512    /// Linux/FreeBSD: `O_DIRECT`, macOS: `F_NOCACHE`. Windows: use `--iocp`
513    /// with `--direct-io`. Default: false.
514    #[serde(default)]
515    pub filesystem_direct_io: bool,
516    /// Minimum number of file segments to batch before using io_uring.
517    /// Below this threshold, pwritev may be cheaper. Default: 4.
518    #[serde(default = "default_io_uring_batch_threshold")]
519    pub io_uring_batch_threshold: usize,
520    /// IOCP concurrent thread count (0 = system default). Only used when
521    /// `storage_mode` is `Iocp`. Default: 0.
522    #[serde(default)]
523    pub iocp_concurrent_threads: u32,
524    /// Enable FILE_FLAG_NO_BUFFERING for IOCP I/O, bypassing the OS page cache.
525    /// Requires sector-aligned writes. Default: false.
526    #[serde(default)]
527    pub iocp_direct_io: bool,
528    // ── Hashing & piece picking ──
529    /// Number of concurrent piece hash verification threads (default: 2).
530    #[serde(default = "default_hashing_threads")]
531    pub hashing_threads: usize,
532    /// Maximum per-peer request queue depth (default: 250).
533    #[serde(default = "default_max_request_queue_depth")]
534    pub max_request_queue_depth: usize,
535    /// Initial per-peer request queue depth (default: 128). Higher values let
536    /// peers reach full throughput faster by skipping slow-start ramp-up.
537    #[serde(default = "default_initial_queue_depth")]
538    pub initial_queue_depth: usize,
539    /// Request queue time multiplier in seconds (default: 3.0).
540    ///
541    /// **Deprecated**: This field is retained for backward compatibility with
542    /// existing config files. The pipeline now uses a fixed-depth model where
543    /// queue depth equals `initial_queue_depth` for the lifetime of the
544    /// connection; this value is no longer used in depth computation.
545    #[serde(default = "default_request_queue_time")]
546    pub request_queue_time: f64,
547    /// Block request timeout in seconds before the request is considered
548    /// lost and re-issued (default: 60).
549    #[serde(default = "default_block_request_timeout")]
550    pub block_request_timeout_secs: u32,
551    /// Maximum concurrent `FileStream` readers. Controls how many simultaneous
552    /// file-streaming reads can proceed (default: 8).
553    #[serde(default = "default_max_concurrent_streams")]
554    pub max_concurrent_stream_reads: usize,
555    /// Automatically switch to sequential piece picking when too many partial
556    /// pieces accumulate. Uses hysteresis (1.6x activate / 1.3x deactivate).
557    #[serde(default = "default_true")]
558    pub auto_sequential: bool,
559    /// In end-game mode, cancel duplicate requests when a piece completes.
560    /// When false, both copies download — wastes bandwidth but finishes faster
561    /// on unreliable peers. Default: true.
562    #[serde(default = "default_strict_end_game")]
563    pub strict_end_game: bool,
564    /// Maximum concurrent web seed connections per torrent (default: 4).
565    #[serde(default = "default_max_web_seeds")]
566    pub max_web_seeds: usize,
567    /// Completed piece count below which the picker uses random selection
568    /// to promote piece diversity in the swarm. Default: 4.
569    #[serde(default = "default_initial_picker_threshold")]
570    pub initial_picker_threshold: u32,
571    /// Seconds to download a piece — if a peer is faster, it gets exclusive
572    /// assignment (no block splitting). Default: 20.
573    #[serde(default = "default_whole_pieces_threshold")]
574    pub whole_pieces_threshold: u32,
575    /// Seconds without data from a peer before marking it as snubbed.
576    /// Snubbed peers get queue depth clamped to 1. Default: 60.
577    #[serde(default = "default_snub_timeout_secs")]
578    pub snub_timeout_secs: u32,
579    /// Number of pieces ahead of the streaming cursor to prioritize (default: 8).
580    #[serde(default = "default_readahead_pieces")]
581    pub readahead_pieces: u32,
582    /// Escalate streaming piece requests that exceed the mean RTT. Default: true.
583    #[serde(default = "default_true")]
584    pub streaming_timeout_escalation: bool,
585    /// Steal blocks from peers this many times slower than the requesting peer (default: 10.0).
586    /// Set to 0.0 to disable stealing.
587    #[serde(default = "default_steal_threshold_ratio")]
588    pub steal_threshold_ratio: f64,
589    /// Enable per-block stealing: fast peers can steal individual unrequested
590    /// blocks from pieces reserved by slower peers (default: true).
591    #[serde(default = "default_use_block_stealing")]
592    pub use_block_stealing: bool,
593    /// Seconds between steal-queue population scans. Every N seconds, all
594    /// in-flight pieces are pushed into the steal queue so fast peers can
595    /// steal blocks mid-download (not just at endgame). 0 = disabled.
596    /// Default: 2.
597    #[serde(default = "default_steal_stale_piece_secs")]
598    pub steal_stale_piece_secs: u64,
599    /// M149: Steal threshold multiplier when >90% complete (endgame).
600    /// Pieces taking longer than swarm_avg * this value are stolen. Default: 3.0.
601    #[serde(default = "default_steal_threshold_endgame")]
602    pub steal_threshold_endgame: f64,
603    /// M149: Minimum per-peer pipeline depth (requests in flight). Default: 16.
604    #[serde(default = "default_min_pipeline_depth")]
605    pub min_pipeline_depth: u32,
606    /// M149: Maximum per-peer pipeline depth (requests in flight). Default: 512.
607    #[serde(default = "default_max_pipeline_depth")]
608    pub max_pipeline_depth: u32,
609    /// M149: Seconds of data to buffer in the pipeline per peer. Used to compute
610    /// dynamic depth: depth = (download_rate / block_size) * target_buffer_secs.
611    #[serde(default = "default_target_buffer_secs")]
612    pub target_buffer_secs: f64,
613    /// Fixed per-peer pipeline depth (number of concurrent requests per peer).
614    /// Replaces the old AIMD dynamic depth system. rqbit uses a fixed
615    /// `Semaphore(128)` per peer — simpler and faster. This setting allows
616    /// benchmarking different fixed depths. Default: 128.
617    #[serde(default = "default_fixed_pipeline_depth")]
618    pub fixed_pipeline_depth: usize,
619
620    // ── Piece picker enhancements (M44) ──
621    /// Prefer pieces adjacent to those already downloaded for improved sequential
622    /// disk access patterns (4 MiB extent groups). Default: true.
623    #[serde(default = "default_true")]
624    pub piece_extent_affinity: bool,
625    /// Enable BEP 6 SuggestPiece: suggest newly verified pieces to peers that
626    /// don't have them, improving piece diversity in the swarm. Default: false.
627    #[serde(default)]
628    pub suggest_mode: bool,
629    /// Maximum SuggestPiece messages per peer to avoid flooding (default: 10).
630    #[serde(default = "default_max_suggest_pieces")]
631    pub max_suggest_pieces: usize,
632    /// Predictive piece announce delay in milliseconds. When > 0, a Have message
633    /// is sent before hash verification completes, reducing piece availability
634    /// latency at the cost of a possible false announce. Default: 0 (disabled).
635    #[serde(default = "default_predictive_piece_announce_ms")]
636    pub predictive_piece_announce_ms: u64,
637
638    // ── Proxy ──
639    /// Proxy configuration for peer and tracker connections. Default: no proxy.
640    #[serde(default)]
641    pub proxy: ProxyConfig,
642    /// Force all connections through the configured proxy. Disables listen
643    /// sockets, UPnP, NAT-PMP, DHT, and LSD. Default: false.
644    #[serde(default)]
645    pub force_proxy: bool,
646    /// Check tracker IP addresses against the IP filter. When false, trackers
647    /// are exempt from IP filtering. Default: true.
648    #[serde(default = "default_true")]
649    pub apply_ip_filter_to_trackers: bool,
650
651    // ── DHT tuning ──
652    /// Maximum DHT queries per second to control network traffic (default: 50).
653    #[serde(default = "default_dht_qps")]
654    pub dht_queries_per_second: usize,
655    /// Timeout in seconds for a single DHT query before it is abandoned (default: 5).
656    #[serde(default = "default_dht_timeout")]
657    pub dht_query_timeout_secs: u64,
658    /// BEP 42: Enforce node ID verification in DHT routing table.
659    /// Disabled by default: too many real DHT nodes lack BEP 42-compliant IDs.
660    #[serde(default)]
661    pub dht_enforce_node_id: bool,
662    /// BEP 42: Restrict DHT routing table to one node per IP.
663    #[serde(default = "default_true")]
664    pub dht_restrict_routing_ips: bool,
665    /// Maximum number of BEP 44 items stored in the DHT (immutable + mutable).
666    #[serde(default = "default_dht_max_items")]
667    pub dht_max_items: usize,
668    /// Lifetime of BEP 44 DHT items in seconds before expiry (default: 7200 = 2 hours).
669    #[serde(default = "default_dht_item_lifetime")]
670    pub dht_item_lifetime_secs: u64,
671    /// Interval in seconds for periodic sample_infohashes queries (BEP 51).
672    /// 0 = disabled (default). Non-zero enables background DHT indexing.
673    #[serde(default = "default_dht_sample_interval")]
674    pub dht_sample_infohashes_interval: u64,
675    /// BEP 43: Run DHT in read-only mode. Read-only nodes can query the DHT
676    /// but do not store data or announce. Other nodes should not add us to
677    /// their routing tables. Useful for resource-constrained clients.
678    #[serde(default)]
679    pub dht_read_only: bool,
680
681    // ── NAT tuning ──
682    /// UPnP lease duration in seconds (default: 3600).
683    #[serde(default = "default_upnp_lease")]
684    pub upnp_lease_duration: u32,
685    /// NAT-PMP mapping lifetime in seconds (default: 7200).
686    #[serde(default = "default_natpmp_lifetime")]
687    pub natpmp_lifetime: u32,
688
689    // ── uTP tuning ──
690    /// Maximum concurrent uTP connections (default: 256).
691    #[serde(default = "default_utp_max_conns")]
692    pub utp_max_connections: usize,
693
694    // ── I2P ──
695    /// Enable I2P anonymous network support (requires SAM bridge).
696    #[serde(default)]
697    pub enable_i2p: bool,
698    /// SAM bridge hostname (default: "127.0.0.1").
699    #[serde(default = "default_i2p_hostname")]
700    pub i2p_hostname: String,
701    /// SAM bridge port (default: 7656).
702    #[serde(default = "default_i2p_port")]
703    pub i2p_port: u16,
704    /// Number of inbound I2P tunnels (1-16, default: 3).
705    #[serde(default = "default_i2p_tunnel_quantity")]
706    pub i2p_inbound_quantity: u8,
707    /// Number of outbound I2P tunnels (1-16, default: 3).
708    #[serde(default = "default_i2p_tunnel_quantity")]
709    pub i2p_outbound_quantity: u8,
710    /// Number of hops in inbound I2P tunnels (0-7, default: 3).
711    #[serde(default = "default_i2p_tunnel_length")]
712    pub i2p_inbound_length: u8,
713    /// Number of hops in outbound I2P tunnels (0-7, default: 3).
714    #[serde(default = "default_i2p_tunnel_length")]
715    pub i2p_outbound_length: u8,
716    /// Allow mixing I2P and clearnet peers in the same torrent.
717    /// When false (default), I2P-enabled torrents only connect to I2P peers.
718    #[serde(default)]
719    pub allow_i2p_mixed: bool,
720
721    // ── SSL torrents (M42) ──
722    /// SSL listen port for SSL torrent incoming connections.
723    /// 0 = disabled (no SSL listener). When set, a TLS listener is bound
724    /// on this port for torrents with `ssl-cert` in their info dict.
725    #[serde(default = "default_ssl_listen_port")]
726    pub ssl_listen_port: u16,
727    /// Path to the PEM-encoded certificate file for SSL torrent connections.
728    /// If not set, a self-signed certificate is auto-generated on first use
729    /// and stored in `resume_data_dir` (or a temp directory).
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub ssl_cert_path: Option<PathBuf>,
732    /// Path to the PEM-encoded private key file for SSL torrent connections.
733    #[serde(default, skip_serializing_if = "Option::is_none")]
734    pub ssl_key_path: Option<PathBuf>,
735
736    // ── Choking algorithms (M43) ──
737    /// Algorithm for ranking peers during seed-mode choking.
738    #[serde(default = "default_seed_choking_algorithm")]
739    pub seed_choking_algorithm: SeedChokingAlgorithm,
740    /// Algorithm for determining the number of unchoke slots.
741    #[serde(default = "default_choking_algorithm")]
742    pub choking_algorithm: ChokingAlgorithm,
743
744    // ── Peer connections ──
745    /// Maximum peer connections per torrent (default: 128).
746    #[serde(default = "default_max_peers_per_torrent")]
747    pub max_peers_per_torrent: usize,
748
749    /// M133: Seconds without any wire message before disconnecting a peer.
750    /// Matches rqbit's 10s read timeout. 0 = disabled. Default: 10.
751    #[serde(default = "default_peer_read_timeout_secs")]
752    pub peer_read_timeout_secs: u64,
753    /// M133: Seconds before a stalled outgoing write disconnects a peer.
754    /// 0 = disabled. Default: 10.
755    #[serde(default = "default_peer_write_timeout_secs")]
756    pub peer_write_timeout_secs: u64,
757
758    /// M137: Data contribution timeout — seconds without receiving a Piece
759    /// message before disconnecting. Set to 0 to disable. Default: 60.
760    #[serde(default = "default_data_contribution_timeout")]
761    pub data_contribution_timeout_secs: u64,
762
763    /// M138: Maximum peers to evict per choke rotation tick (0 = disabled).
764    #[serde(default = "default_choke_rotation_max_evictions")]
765    pub choke_rotation_max_evictions: u32,
766
767    /// M138: Maximum concurrent outbound peer connections (throttles connect ramp).
768    #[serde(default = "default_max_concurrent_connects")]
769    pub max_concurrent_connects: u16,
770
771    /// M147: Seconds without TCP SYN-ACK before soft reap disconnects a connecting
772    /// peer. Peers that have received SYN-ACK get the full `peer_connect_timeout`.
773    #[serde(default = "default_connect_soft_timeout")]
774    pub connect_soft_timeout: u64,
775
776    // ── Security ──
777    /// Enable SSRF mitigation: restrict localhost tracker paths, block
778    /// public-to-private redirects, and reject query strings on local web seeds.
779    #[serde(default = "default_true")]
780    pub ssrf_mitigation: bool,
781    /// Allow internationalised (non-ASCII) domain names in tracker/web seed URLs.
782    #[serde(default)]
783    pub allow_idna: bool,
784    /// Require HTTPS for HTTP tracker announces (UDP trackers are unaffected).
785    #[serde(default = "default_true")]
786    pub validate_https_trackers: bool,
787    /// Maximum BEP 9 metadata size in bytes that will be accepted from peers.
788    /// Protects against OOM from peers claiming enormous metadata. Default: 4 MiB.
789    #[serde(default = "default_max_metadata_size")]
790    pub max_metadata_size: u64,
791    /// Maximum wire protocol message size in bytes. Messages exceeding this are
792    /// rejected by the codec. Default: 16 MiB.
793    #[serde(default = "default_max_message_size")]
794    pub max_message_size: usize,
795    /// Maximum accepted piece length when adding a torrent. Rejects torrents
796    /// with piece sizes above this limit. Default: 32 MiB.
797    #[serde(default = "default_max_piece_length")]
798    pub max_piece_length: u64,
799    /// Maximum outstanding incoming requests per peer. When a peer sends more
800    /// Request messages than this without them being served, excess requests
801    /// are dropped. Default: 500.
802    #[serde(default = "default_max_outstanding_requests")]
803    pub max_outstanding_requests: usize,
804    /// Maximum number of pieces simultaneously in-flight (downloaded but not
805    /// yet verified). Caps memory usage for in-progress pieces. When the cap
806    /// is reached, the piece selector only returns blocks from already-in-flight
807    /// pieces. Default: 512.
808    #[serde(default = "default_max_in_flight_pieces")]
809    pub max_in_flight_pieces: usize,
810    /// Timeout in seconds for outbound TCP peer connections.
811    /// Default 10. Set to 0 to use the OS default (~2 minutes on Linux).
812    #[serde(default = "default_peer_connect_timeout")]
813    pub peer_connect_timeout: u64,
814    /// DSCP (Differentiated Services Code Point) value for peer traffic sockets.
815    /// Applied to TCP listeners, outbound TCP connections, uTP sockets, and UDP tracker sockets.
816    /// Default 0x08 (CS1/scavenger — low-priority background). Set to 0 to disable DSCP marking.
817    #[serde(default = "default_peer_dscp")]
818    pub peer_dscp: u8,
819
820    // ── Session Stats (M50) ──
821    /// Interval in milliseconds between `SessionStatsAlert` emissions.
822    /// Default 1000 (1 second). Set to 0 to disable periodic stats alerts.
823    #[serde(default = "default_stats_report_interval")]
824    pub stats_report_interval: u64,
825
826    // ── Runtime tuning (M95) ──
827    /// Number of tokio worker threads. Default: min(available cores, 8).
828    /// Set to 0 to use tokio's default (= available_parallelism()).
829    #[serde(default = "default_runtime_worker_threads")]
830    pub runtime_worker_threads: usize,
831    /// Pin tokio worker threads to CPU cores for cache locality. Default: true.
832    #[serde(default = "default_true")]
833    pub pin_cores: bool,
834
835    // ── Lock diagnostics (M120) ──
836    /// Warning threshold in milliseconds for lock hold duration.
837    /// When a hot-path lock is held longer than this, a tracing warning is
838    /// emitted. Set to 0 to disable timing entirely (zero overhead).
839    /// Default: 50.
840    #[serde(default = "default_lock_warn_threshold_ms")]
841    pub lock_warn_threshold_ms: u64,
842
843    // ── DHT bootstrap (M56) ──
844    /// Previously saved DHT routing table nodes for fast bootstrap.
845    /// These are prepended to the bootstrap node list on startup so that
846    /// peer discovery starts instantly instead of bootstrapping from scratch.
847    /// Runtime-injected, not serialized.
848    #[serde(skip)]
849    pub dht_saved_nodes: Vec<String>,
850    /// BEP 42-compliant DHT node ID from previous session.
851    /// Reusing the same ID avoids routing table regeneration on every startup.
852    /// Runtime-injected, not serialized.
853    #[serde(skip)]
854    pub dht_node_id: Option<irontide_core::Id20>,
855}
856
857impl Default for Settings {
858    fn default() -> Self {
859        Self {
860            // General
861            listen_port: 42020,
862            download_dir: PathBuf::from("."),
863            max_torrents: 100,
864            resume_data_dir: None,
865            save_resume_interval_secs: 300,
866            // Protocol features
867            enable_dht: true,
868            enable_pex: true,
869            enable_lsd: true,
870            enable_fast_extension: true,
871            enable_utp: true,
872            enable_upnp: true,
873            enable_natpmp: true,
874            enable_ipv6: true,
875            enable_web_seed: true,
876            enable_holepunch: true,
877            enable_bep40_eviction: true,
878            encryption_mode: EncryptionMode::Disabled,
879            anonymous_mode: false,
880            external_ip: None,
881            // Seeding
882            seed_ratio_limit: None,
883            default_super_seeding: false,
884            default_share_mode: false,
885            upload_only_announce: true,
886            // Rate limiting
887            upload_rate_limit: 0,
888            download_rate_limit: 0,
889            tcp_upload_rate_limit: 0,
890            tcp_download_rate_limit: 0,
891            utp_upload_rate_limit: 0,
892            utp_download_rate_limit: 0,
893            auto_upload_slots: true,
894            auto_upload_slots_min: 2,
895            auto_upload_slots_max: 20,
896            mixed_mode_algorithm: MixedModeAlgorithm::PeerProportional,
897            // Queue management
898            active_downloads: 3,
899            active_seeds: 5,
900            active_limit: 500,
901            active_checking: 1,
902            dont_count_slow_torrents: true,
903            inactive_down_rate: 2048,
904            inactive_up_rate: 2048,
905            auto_manage_interval: 30,
906            auto_manage_startup: 60,
907            auto_manage_prefer_seeds: false,
908            // Alerts
909            alert_mask: AlertCategory::ALL,
910            alert_channel_size: 1024,
911            // Smart banning
912            smart_ban_max_failures: 3,
913            smart_ban_parole: true,
914            // Disk I/O
915            disk_io_threads: default_disk_io_threads(),
916            max_blocking_threads: default_max_blocking_threads(),
917            storage_mode: StorageMode::Auto,
918            preallocate_mode: None,
919            disk_cache_size: 16 * 1024 * 1024,
920            disk_write_cache_ratio: 0.5,
921            disk_channel_capacity: 512,
922            buffer_pool_capacity: 64 * 1024 * 1024,
923            enable_mlock: cfg!(unix),
924            io_uring_sq_depth: 256,
925            io_uring_direct_io: false,
926            filesystem_direct_io: false,
927            io_uring_batch_threshold: 4,
928            iocp_concurrent_threads: 0,
929            iocp_direct_io: false,
930            // Hashing & piece picking
931            hashing_threads: default_hashing_threads(),
932            max_request_queue_depth: 250,
933            initial_queue_depth: 128,
934            request_queue_time: 3.0,
935            block_request_timeout_secs: 60,
936            max_concurrent_stream_reads: 8,
937            auto_sequential: true,
938            steal_threshold_ratio: 10.0,
939            use_block_stealing: true,
940            steal_stale_piece_secs: 2,
941            steal_threshold_endgame: 3.0,
942            min_pipeline_depth: 16,
943            max_pipeline_depth: 512,
944            target_buffer_secs: 2.0,
945            fixed_pipeline_depth: 128,
946            strict_end_game: true,
947            max_web_seeds: 4,
948            initial_picker_threshold: 4,
949            whole_pieces_threshold: 20,
950            snub_timeout_secs: 15,
951            readahead_pieces: 8,
952            streaming_timeout_escalation: true,
953            // Piece picker enhancements (M44)
954            piece_extent_affinity: true,
955            suggest_mode: false,
956            max_suggest_pieces: 16,
957            predictive_piece_announce_ms: 0,
958            // Proxy
959            proxy: ProxyConfig::default(),
960            force_proxy: false,
961            apply_ip_filter_to_trackers: true,
962            // DHT tuning
963            dht_queries_per_second: 50,
964            dht_query_timeout_secs: 5,
965            dht_enforce_node_id: false,
966            dht_restrict_routing_ips: true,
967            dht_max_items: 700,
968            dht_item_lifetime_secs: 7200,
969            dht_sample_infohashes_interval: 0,
970            dht_read_only: false,
971            // NAT tuning
972            upnp_lease_duration: 3600,
973            natpmp_lifetime: 7200,
974            // uTP tuning
975            utp_max_connections: 256,
976            // I2P
977            enable_i2p: false,
978            i2p_hostname: "127.0.0.1".into(),
979            i2p_port: 7656,
980            i2p_inbound_quantity: 3,
981            i2p_outbound_quantity: 3,
982            i2p_inbound_length: 3,
983            i2p_outbound_length: 3,
984            allow_i2p_mixed: false,
985            // SSL torrents
986            ssl_listen_port: 0,
987            ssl_cert_path: None,
988            ssl_key_path: None,
989            // Choking algorithms
990            seed_choking_algorithm: SeedChokingAlgorithm::FastestUpload,
991            choking_algorithm: ChokingAlgorithm::FixedSlots,
992            // Peer connections
993            max_peers_per_torrent: 128,
994            peer_read_timeout_secs: 10,
995            peer_write_timeout_secs: 10,
996            data_contribution_timeout_secs: 0,
997            choke_rotation_max_evictions: 0,
998            max_concurrent_connects: 128,
999            connect_soft_timeout: 3,
1000            // Security
1001            ssrf_mitigation: true,
1002            allow_idna: false,
1003            validate_https_trackers: true,
1004            max_metadata_size: 4 * 1024 * 1024,
1005            max_message_size: 16 * 1024 * 1024,
1006            max_piece_length: 32 * 1024 * 1024,
1007            max_outstanding_requests: 500,
1008            max_in_flight_pieces: 512,
1009            peer_connect_timeout: 10,
1010            peer_dscp: 0x08,
1011            // Session Stats (M50)
1012            stats_report_interval: 1000,
1013            // Runtime tuning (M95)
1014            runtime_worker_threads: default_runtime_worker_threads(),
1015            pin_cores: true,
1016            // Lock diagnostics (M120)
1017            lock_warn_threshold_ms: 50,
1018            // DHT bootstrap (M56)
1019            dht_saved_nodes: Vec::new(),
1020            dht_node_id: None,
1021        }
1022    }
1023}
1024
1025impl Settings {
1026    /// Preset for constrained/embedded environments.
1027    pub fn min_memory() -> Self {
1028        Self {
1029            disk_cache_size: 8 * 1024 * 1024,
1030            buffer_pool_capacity: 16 * 1024 * 1024,
1031            max_torrents: 20,
1032            max_peers_per_torrent: 30,
1033            active_downloads: 1,
1034            active_seeds: 2,
1035            active_limit: 10,
1036            alert_channel_size: 256,
1037            utp_max_connections: 64,
1038            max_request_queue_depth: 50,
1039            initial_queue_depth: 16,
1040            max_concurrent_stream_reads: 2,
1041            hashing_threads: 1,
1042            disk_io_threads: 1,
1043            dht_max_items: 100,
1044            max_in_flight_pieces: 32,
1045            fixed_pipeline_depth: 32,
1046            ..Self::default()
1047        }
1048    }
1049
1050    /// Preset for desktop/server environments with ample resources.
1051    pub fn high_performance() -> Self {
1052        Self {
1053            disk_cache_size: 256 * 1024 * 1024,
1054            buffer_pool_capacity: 256 * 1024 * 1024,
1055            max_torrents: 2000,
1056            max_peers_per_torrent: 200,
1057            active_downloads: 30,
1058            active_seeds: 100,
1059            active_limit: 2000,
1060            alert_channel_size: 4096,
1061            utp_max_connections: 1024,
1062            max_request_queue_depth: 1000,
1063            initial_queue_depth: 256,
1064            max_concurrent_stream_reads: 32,
1065            hashing_threads: 4,
1066            disk_io_threads: 8,
1067            auto_upload_slots_max: 100,
1068            suggest_mode: true,
1069            steal_threshold_ratio: 5.0,
1070            steal_threshold_endgame: 2.0,
1071            min_pipeline_depth: 16,
1072            max_pipeline_depth: 512,
1073            target_buffer_secs: 2.0,
1074            use_block_stealing: true,
1075            max_in_flight_pieces: 512,
1076            ..Self::default()
1077        }
1078    }
1079
1080    /// Validate settings. Returns error on the first invalid combination found.
1081    pub fn validate(&self) -> crate::Result<()> {
1082        use crate::proxy::ProxyType;
1083
1084        if self.force_proxy && self.proxy.proxy_type == ProxyType::None {
1085            return Err(crate::Error::InvalidSettings(
1086                "force_proxy is enabled but no proxy type is configured".into(),
1087            ));
1088        }
1089
1090        if self.active_downloads > 0
1091            && self.active_limit > 0
1092            && self.active_downloads > self.active_limit
1093        {
1094            return Err(crate::Error::InvalidSettings(
1095                "active_downloads exceeds active_limit".into(),
1096            ));
1097        }
1098
1099        if self.active_seeds > 0 && self.active_limit > 0 && self.active_seeds > self.active_limit {
1100            return Err(crate::Error::InvalidSettings(
1101                "active_seeds exceeds active_limit".into(),
1102            ));
1103        }
1104
1105        if !(0.0..=1.0).contains(&self.disk_write_cache_ratio) {
1106            return Err(crate::Error::InvalidSettings(
1107                "disk_write_cache_ratio must be between 0.0 and 1.0".into(),
1108            ));
1109        }
1110
1111        if self.disk_cache_size < 1024 * 1024 {
1112            return Err(crate::Error::InvalidSettings(
1113                "disk_cache_size must be at least 1 MiB".into(),
1114            ));
1115        }
1116
1117        if self.hashing_threads == 0 {
1118            return Err(crate::Error::InvalidSettings(
1119                "hashing_threads must be at least 1".into(),
1120            ));
1121        }
1122
1123        if self.disk_io_threads == 0 {
1124            return Err(crate::Error::InvalidSettings(
1125                "disk_io_threads must be at least 1".into(),
1126            ));
1127        }
1128
1129        if self.max_blocking_threads == 0 {
1130            return Err(crate::Error::InvalidSettings(
1131                "max_blocking_threads must be at least 1".into(),
1132            ));
1133        }
1134
1135        if self.default_share_mode && !self.enable_fast_extension {
1136            return Err(crate::Error::InvalidSettings(
1137                "share_mode requires enable_fast_extension for RejectRequest messages".into(),
1138            ));
1139        }
1140
1141        // SSL cert/key must both be set or both absent
1142        if self.ssl_cert_path.is_some() != self.ssl_key_path.is_some() {
1143            return Err(crate::Error::InvalidSettings(
1144                "ssl_cert_path and ssl_key_path must both be set or both absent".into(),
1145            ));
1146        }
1147
1148        if self.enable_i2p {
1149            if self.i2p_inbound_quantity == 0 || self.i2p_inbound_quantity > 16 {
1150                return Err(crate::Error::InvalidSettings(
1151                    "i2p_inbound_quantity must be 1-16".into(),
1152                ));
1153            }
1154            if self.i2p_outbound_quantity == 0 || self.i2p_outbound_quantity > 16 {
1155                return Err(crate::Error::InvalidSettings(
1156                    "i2p_outbound_quantity must be 1-16".into(),
1157                ));
1158            }
1159            if self.i2p_inbound_length > 7 {
1160                return Err(crate::Error::InvalidSettings(
1161                    "i2p_inbound_length must be 0-7".into(),
1162                ));
1163            }
1164            if self.i2p_outbound_length > 7 {
1165                return Err(crate::Error::InvalidSettings(
1166                    "i2p_outbound_length must be 0-7".into(),
1167                ));
1168            }
1169        }
1170
1171        if self.runtime_worker_threads > 256 {
1172            return Err(crate::Error::InvalidSettings(
1173                "runtime_worker_threads must be at most 256".into(),
1174            ));
1175        }
1176
1177        Ok(())
1178    }
1179}
1180
1181// ── Sub-config conversions ───────────────────────────────────────────
1182
1183impl From<&Settings> for crate::disk::DiskConfig {
1184    fn from(s: &Settings) -> Self {
1185        Self {
1186            io_threads: s.disk_io_threads,
1187            storage_mode: s.storage_mode,
1188            cache_size: s.disk_cache_size,
1189            write_cache_ratio: s.disk_write_cache_ratio,
1190            channel_capacity: s.disk_channel_capacity,
1191            buffer_pool_capacity: s.buffer_pool_capacity,
1192            enable_mlock: s.enable_mlock,
1193            lock_warn_threshold_ms: s.lock_warn_threshold_ms,
1194            io_uring_sq_depth: s.io_uring_sq_depth,
1195            io_uring_direct_io: s.io_uring_direct_io,
1196            filesystem_direct_io: s.filesystem_direct_io,
1197            io_uring_batch_threshold: s.io_uring_batch_threshold,
1198            iocp_concurrent_threads: s.iocp_concurrent_threads,
1199            iocp_direct_io: s.iocp_direct_io,
1200        }
1201    }
1202}
1203
1204impl From<&Settings> for crate::ban::BanConfig {
1205    fn from(s: &Settings) -> Self {
1206        Self {
1207            max_failures: s.smart_ban_max_failures,
1208            use_parole: s.smart_ban_parole,
1209        }
1210    }
1211}
1212
1213impl Settings {
1214    pub(crate) fn to_dht_config(&self) -> irontide_dht::DhtConfig {
1215        let default = irontide_dht::DhtConfig::default();
1216        let mut bootstrap = self.dht_saved_nodes.clone();
1217        bootstrap.extend(default.bootstrap_nodes.iter().cloned());
1218        irontide_dht::DhtConfig {
1219            bootstrap_nodes: bootstrap,
1220            own_id: self.dht_node_id,
1221            queries_per_second: self.dht_queries_per_second,
1222            query_timeout: std::time::Duration::from_secs(self.dht_query_timeout_secs),
1223            enforce_node_id: self.dht_enforce_node_id,
1224            restrict_routing_ips: self.dht_restrict_routing_ips,
1225            dht_max_items: self.dht_max_items,
1226            dht_item_lifetime_secs: self.dht_item_lifetime_secs,
1227            state_dir: self.resume_data_dir.clone(),
1228            read_only_mode: self.dht_read_only,
1229            ..default
1230        }
1231    }
1232
1233    pub(crate) fn to_dht_config_v6(&self) -> irontide_dht::DhtConfig {
1234        let default = irontide_dht::DhtConfig::default_v6();
1235        let mut bootstrap = self.dht_saved_nodes.clone();
1236        bootstrap.extend(default.bootstrap_nodes.iter().cloned());
1237        irontide_dht::DhtConfig {
1238            bootstrap_nodes: bootstrap,
1239            queries_per_second: self.dht_queries_per_second,
1240            query_timeout: std::time::Duration::from_secs(self.dht_query_timeout_secs),
1241            enforce_node_id: self.dht_enforce_node_id,
1242            restrict_routing_ips: self.dht_restrict_routing_ips,
1243            dht_max_items: self.dht_max_items,
1244            dht_item_lifetime_secs: self.dht_item_lifetime_secs,
1245            state_dir: self.resume_data_dir.clone(),
1246            read_only_mode: self.dht_read_only,
1247            ..default
1248        }
1249    }
1250
1251    pub(crate) fn to_nat_config(&self) -> irontide_nat::NatConfig {
1252        irontide_nat::NatConfig {
1253            enable_upnp: self.enable_upnp,
1254            enable_natpmp: self.enable_natpmp,
1255            upnp_lease_duration: self.upnp_lease_duration,
1256            natpmp_lifetime: self.natpmp_lifetime,
1257        }
1258    }
1259
1260    pub(crate) fn to_utp_config(&self, port: u16) -> irontide_utp::UtpConfig {
1261        irontide_utp::UtpConfig {
1262            bind_addr: std::net::SocketAddr::from(([0, 0, 0, 0], port)),
1263            max_connections: self.utp_max_connections,
1264            dscp: self.peer_dscp,
1265        }
1266    }
1267
1268    pub(crate) fn to_utp_config_v6(&self, port: u16) -> irontide_utp::UtpConfig {
1269        irontide_utp::UtpConfig {
1270            bind_addr: std::net::SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, port)),
1271            max_connections: self.utp_max_connections,
1272            dscp: self.peer_dscp,
1273        }
1274    }
1275
1276    /// Build a `SamTunnelConfig` from the I2P-related settings.
1277    pub(crate) fn to_sam_tunnel_config(&self) -> crate::i2p::SamTunnelConfig {
1278        crate::i2p::SamTunnelConfig {
1279            inbound_quantity: self.i2p_inbound_quantity,
1280            outbound_quantity: self.i2p_outbound_quantity,
1281            inbound_length: self.i2p_inbound_length,
1282            outbound_length: self.i2p_outbound_length,
1283        }
1284    }
1285}
1286
1287// ── PartialEq (manual — f32/f64 fields need special handling) ────────
1288
1289impl PartialEq for Settings {
1290    fn eq(&self, other: &Self) -> bool {
1291        self.listen_port == other.listen_port
1292            && self.download_dir == other.download_dir
1293            && self.max_torrents == other.max_torrents
1294            && self.resume_data_dir == other.resume_data_dir
1295            && self.save_resume_interval_secs == other.save_resume_interval_secs
1296            && self.enable_dht == other.enable_dht
1297            && self.enable_pex == other.enable_pex
1298            && self.enable_lsd == other.enable_lsd
1299            && self.enable_fast_extension == other.enable_fast_extension
1300            && self.enable_utp == other.enable_utp
1301            && self.enable_upnp == other.enable_upnp
1302            && self.enable_natpmp == other.enable_natpmp
1303            && self.enable_ipv6 == other.enable_ipv6
1304            && self.enable_web_seed == other.enable_web_seed
1305            && self.enable_holepunch == other.enable_holepunch
1306            && self.enable_bep40_eviction == other.enable_bep40_eviction
1307            && self.encryption_mode == other.encryption_mode
1308            && self.anonymous_mode == other.anonymous_mode
1309            && self.external_ip == other.external_ip
1310            && self.seed_ratio_limit == other.seed_ratio_limit
1311            && self.default_super_seeding == other.default_super_seeding
1312            && self.default_share_mode == other.default_share_mode
1313            && self.upload_only_announce == other.upload_only_announce
1314            && self.upload_rate_limit == other.upload_rate_limit
1315            && self.download_rate_limit == other.download_rate_limit
1316            && self.tcp_upload_rate_limit == other.tcp_upload_rate_limit
1317            && self.tcp_download_rate_limit == other.tcp_download_rate_limit
1318            && self.utp_upload_rate_limit == other.utp_upload_rate_limit
1319            && self.utp_download_rate_limit == other.utp_download_rate_limit
1320            && self.auto_upload_slots == other.auto_upload_slots
1321            && self.auto_upload_slots_min == other.auto_upload_slots_min
1322            && self.auto_upload_slots_max == other.auto_upload_slots_max
1323            && self.mixed_mode_algorithm == other.mixed_mode_algorithm
1324            && self.active_downloads == other.active_downloads
1325            && self.active_seeds == other.active_seeds
1326            && self.active_limit == other.active_limit
1327            && self.active_checking == other.active_checking
1328            && self.dont_count_slow_torrents == other.dont_count_slow_torrents
1329            && self.inactive_down_rate == other.inactive_down_rate
1330            && self.inactive_up_rate == other.inactive_up_rate
1331            && self.auto_manage_interval == other.auto_manage_interval
1332            && self.auto_manage_startup == other.auto_manage_startup
1333            && self.auto_manage_prefer_seeds == other.auto_manage_prefer_seeds
1334            && self.alert_mask == other.alert_mask
1335            && self.alert_channel_size == other.alert_channel_size
1336            && self.smart_ban_max_failures == other.smart_ban_max_failures
1337            && self.smart_ban_parole == other.smart_ban_parole
1338            && self.disk_io_threads == other.disk_io_threads
1339            && self.max_blocking_threads == other.max_blocking_threads
1340            && self.storage_mode == other.storage_mode
1341            && self.disk_cache_size == other.disk_cache_size
1342            && self.disk_write_cache_ratio.to_bits() == other.disk_write_cache_ratio.to_bits()
1343            && self.disk_channel_capacity == other.disk_channel_capacity
1344            && self.buffer_pool_capacity == other.buffer_pool_capacity
1345            && self.enable_mlock == other.enable_mlock
1346            && self.hashing_threads == other.hashing_threads
1347            && self.max_request_queue_depth == other.max_request_queue_depth
1348            && self.initial_queue_depth == other.initial_queue_depth
1349            && self.request_queue_time.to_bits() == other.request_queue_time.to_bits()
1350            && self.block_request_timeout_secs == other.block_request_timeout_secs
1351            && self.max_concurrent_stream_reads == other.max_concurrent_stream_reads
1352            && self.auto_sequential == other.auto_sequential
1353            && self.steal_threshold_ratio.to_bits() == other.steal_threshold_ratio.to_bits()
1354            && self.use_block_stealing == other.use_block_stealing
1355            && self.steal_stale_piece_secs == other.steal_stale_piece_secs
1356            && self.steal_threshold_endgame.to_bits() == other.steal_threshold_endgame.to_bits()
1357            && self.min_pipeline_depth == other.min_pipeline_depth
1358            && self.max_pipeline_depth == other.max_pipeline_depth
1359            && self.target_buffer_secs.to_bits() == other.target_buffer_secs.to_bits()
1360            && self.fixed_pipeline_depth == other.fixed_pipeline_depth
1361            && self.strict_end_game == other.strict_end_game
1362            && self.max_web_seeds == other.max_web_seeds
1363            && self.initial_picker_threshold == other.initial_picker_threshold
1364            && self.whole_pieces_threshold == other.whole_pieces_threshold
1365            && self.snub_timeout_secs == other.snub_timeout_secs
1366            && self.readahead_pieces == other.readahead_pieces
1367            && self.streaming_timeout_escalation == other.streaming_timeout_escalation
1368            && self.piece_extent_affinity == other.piece_extent_affinity
1369            && self.suggest_mode == other.suggest_mode
1370            && self.max_suggest_pieces == other.max_suggest_pieces
1371            && self.predictive_piece_announce_ms == other.predictive_piece_announce_ms
1372            && self.force_proxy == other.force_proxy
1373            && self.apply_ip_filter_to_trackers == other.apply_ip_filter_to_trackers
1374            && self.dht_queries_per_second == other.dht_queries_per_second
1375            && self.dht_query_timeout_secs == other.dht_query_timeout_secs
1376            && self.dht_enforce_node_id == other.dht_enforce_node_id
1377            && self.dht_restrict_routing_ips == other.dht_restrict_routing_ips
1378            && self.dht_max_items == other.dht_max_items
1379            && self.dht_item_lifetime_secs == other.dht_item_lifetime_secs
1380            && self.dht_sample_infohashes_interval == other.dht_sample_infohashes_interval
1381            && self.dht_read_only == other.dht_read_only
1382            && self.upnp_lease_duration == other.upnp_lease_duration
1383            && self.natpmp_lifetime == other.natpmp_lifetime
1384            && self.utp_max_connections == other.utp_max_connections
1385            && self.enable_i2p == other.enable_i2p
1386            && self.i2p_hostname == other.i2p_hostname
1387            && self.i2p_port == other.i2p_port
1388            && self.i2p_inbound_quantity == other.i2p_inbound_quantity
1389            && self.i2p_outbound_quantity == other.i2p_outbound_quantity
1390            && self.i2p_inbound_length == other.i2p_inbound_length
1391            && self.i2p_outbound_length == other.i2p_outbound_length
1392            && self.allow_i2p_mixed == other.allow_i2p_mixed
1393            && self.ssl_listen_port == other.ssl_listen_port
1394            && self.ssl_cert_path == other.ssl_cert_path
1395            && self.ssl_key_path == other.ssl_key_path
1396            && self.seed_choking_algorithm == other.seed_choking_algorithm
1397            && self.choking_algorithm == other.choking_algorithm
1398            && self.max_peers_per_torrent == other.max_peers_per_torrent
1399            && self.peer_read_timeout_secs == other.peer_read_timeout_secs
1400            && self.peer_write_timeout_secs == other.peer_write_timeout_secs
1401            && self.data_contribution_timeout_secs == other.data_contribution_timeout_secs
1402            && self.choke_rotation_max_evictions == other.choke_rotation_max_evictions
1403            && self.max_concurrent_connects == other.max_concurrent_connects
1404            && self.connect_soft_timeout == other.connect_soft_timeout
1405            && self.ssrf_mitigation == other.ssrf_mitigation
1406            && self.allow_idna == other.allow_idna
1407            && self.validate_https_trackers == other.validate_https_trackers
1408            && self.max_metadata_size == other.max_metadata_size
1409            && self.max_message_size == other.max_message_size
1410            && self.max_piece_length == other.max_piece_length
1411            && self.max_outstanding_requests == other.max_outstanding_requests
1412            && self.max_in_flight_pieces == other.max_in_flight_pieces
1413            && self.peer_connect_timeout == other.peer_connect_timeout
1414            && self.peer_dscp == other.peer_dscp
1415            && self.stats_report_interval == other.stats_report_interval
1416            && self.runtime_worker_threads == other.runtime_worker_threads
1417            && self.pin_cores == other.pin_cores
1418            && self.dht_saved_nodes == other.dht_saved_nodes
1419            && self.dht_node_id == other.dht_node_id
1420    }
1421}
1422
1423// ── Tests ────────────────────────────────────────────────────────────
1424
1425#[cfg(test)]
1426mod tests {
1427    use super::*;
1428
1429    #[test]
1430    fn default_settings_values() {
1431        let s = Settings::default();
1432        assert_eq!(s.listen_port, 42020);
1433        assert_eq!(s.download_dir, PathBuf::from("."));
1434        assert_eq!(s.max_torrents, 100);
1435        assert!(s.resume_data_dir.is_none());
1436        assert_eq!(s.save_resume_interval_secs, 300);
1437        assert!(s.enable_dht);
1438        assert!(s.enable_pex);
1439        assert!(s.enable_lsd);
1440        assert!(s.enable_fast_extension);
1441        assert!(s.enable_utp);
1442        assert!(s.enable_upnp);
1443        assert!(s.enable_natpmp);
1444        assert!(s.enable_ipv6);
1445        assert!(s.enable_web_seed);
1446        assert_eq!(s.encryption_mode, EncryptionMode::Disabled);
1447        assert!(!s.anonymous_mode);
1448        assert!(s.seed_ratio_limit.is_none());
1449        assert!(!s.default_super_seeding);
1450        assert!(!s.default_share_mode);
1451        assert!(s.upload_only_announce);
1452        assert_eq!(s.upload_rate_limit, 0);
1453        assert_eq!(s.download_rate_limit, 0);
1454        assert!(s.auto_upload_slots);
1455        assert_eq!(s.active_downloads, 3);
1456        assert_eq!(s.active_seeds, 5);
1457        assert_eq!(s.active_limit, 500);
1458        assert_eq!(s.active_checking, 1);
1459        assert!(s.dont_count_slow_torrents);
1460        assert_eq!(s.alert_mask, AlertCategory::ALL);
1461        assert_eq!(s.alert_channel_size, 1024);
1462        assert_eq!(s.smart_ban_max_failures, 3);
1463        assert!(s.smart_ban_parole);
1464        assert_eq!(s.disk_io_threads, default_disk_io_threads());
1465        assert_eq!(s.max_blocking_threads, default_max_blocking_threads());
1466        assert_eq!(s.storage_mode, StorageMode::Auto);
1467        assert_eq!(s.disk_cache_size, 16 * 1024 * 1024);
1468        assert!((s.disk_write_cache_ratio - 0.5).abs() < f32::EPSILON);
1469        assert_eq!(s.disk_channel_capacity, 512);
1470        assert_eq!(s.hashing_threads, default_hashing_threads());
1471        assert_eq!(s.max_request_queue_depth, 250);
1472        assert_eq!(s.initial_queue_depth, 128);
1473        assert!((s.request_queue_time - 3.0).abs() < f64::EPSILON);
1474        assert_eq!(s.block_request_timeout_secs, 60);
1475        assert_eq!(s.max_concurrent_stream_reads, 8);
1476        assert!(!s.force_proxy);
1477        assert!(s.apply_ip_filter_to_trackers);
1478        assert_eq!(s.dht_queries_per_second, 50);
1479        assert_eq!(s.dht_query_timeout_secs, 5);
1480        assert!(!s.dht_enforce_node_id);
1481        assert!(s.dht_restrict_routing_ips);
1482        assert_eq!(s.upnp_lease_duration, 3600);
1483        assert_eq!(s.natpmp_lifetime, 7200);
1484        assert_eq!(s.utp_max_connections, 256);
1485        assert_eq!(s.mixed_mode_algorithm, MixedModeAlgorithm::PeerProportional);
1486        assert!(s.auto_sequential);
1487        assert!(s.strict_end_game);
1488        assert_eq!(s.max_web_seeds, 4);
1489        assert_eq!(s.initial_picker_threshold, 4);
1490        assert_eq!(s.whole_pieces_threshold, 20);
1491        assert_eq!(s.snub_timeout_secs, 15);
1492        assert_eq!(s.readahead_pieces, 8);
1493        assert!(s.streaming_timeout_escalation);
1494        assert_eq!(s.max_peers_per_torrent, 128);
1495        assert_eq!(s.runtime_worker_threads, default_runtime_worker_threads());
1496        assert!(s.pin_cores);
1497    }
1498
1499    #[test]
1500    fn min_memory_preset() {
1501        let s = Settings::min_memory();
1502        assert_eq!(s.disk_cache_size, 8 * 1024 * 1024);
1503        assert_eq!(s.max_torrents, 20);
1504        assert_eq!(s.max_peers_per_torrent, 30);
1505        assert_eq!(s.active_downloads, 1);
1506        assert_eq!(s.active_seeds, 2);
1507        assert_eq!(s.active_limit, 10);
1508        assert_eq!(s.alert_channel_size, 256);
1509        assert_eq!(s.utp_max_connections, 64);
1510        assert_eq!(s.max_request_queue_depth, 50);
1511        assert_eq!(s.initial_queue_depth, 16);
1512        assert_eq!(s.max_concurrent_stream_reads, 2);
1513        assert_eq!(s.hashing_threads, 1);
1514        assert_eq!(s.disk_io_threads, 1);
1515    }
1516
1517    #[test]
1518    fn high_performance_preset() {
1519        let s = Settings::high_performance();
1520        assert_eq!(s.disk_cache_size, 256 * 1024 * 1024);
1521        assert_eq!(s.max_torrents, 2000);
1522        assert_eq!(s.max_peers_per_torrent, 200);
1523        assert_eq!(s.active_downloads, 30);
1524        assert_eq!(s.active_seeds, 100);
1525        assert_eq!(s.active_limit, 2000);
1526        assert_eq!(s.alert_channel_size, 4096);
1527        assert_eq!(s.utp_max_connections, 1024);
1528        assert_eq!(s.max_request_queue_depth, 1000);
1529        assert_eq!(s.initial_queue_depth, 256);
1530        assert_eq!(s.max_concurrent_stream_reads, 32);
1531        assert_eq!(s.hashing_threads, 4);
1532        assert_eq!(s.disk_io_threads, 8);
1533        assert_eq!(s.auto_upload_slots_max, 100);
1534    }
1535
1536    #[test]
1537    fn json_round_trip() {
1538        let original = Settings::default();
1539        let json = serde_json::to_string(&original).unwrap();
1540        let decoded: Settings = serde_json::from_str(&json).unwrap();
1541        assert_eq!(original, decoded);
1542    }
1543
1544    #[test]
1545    fn json_round_trip_presets() {
1546        // Verify all presets survive JSON serialization
1547        for original in [Settings::min_memory(), Settings::high_performance()] {
1548            let json = serde_json::to_string(&original).unwrap();
1549            let decoded: Settings = serde_json::from_str(&json).unwrap();
1550            assert_eq!(original, decoded);
1551        }
1552    }
1553
1554    #[test]
1555    fn json_missing_fields_use_defaults() {
1556        // An empty JSON object should deserialize to defaults (via serde(default))
1557        let decoded: Settings = serde_json::from_str("{}").unwrap();
1558        assert_eq!(decoded, Settings::default());
1559    }
1560
1561    #[test]
1562    fn validation_force_proxy_no_proxy() {
1563        let mut s = Settings::default();
1564        s.force_proxy = true;
1565        // proxy_type defaults to None
1566        let err = s.validate().unwrap_err();
1567        assert!(err.to_string().contains("force_proxy"));
1568    }
1569
1570    #[test]
1571    fn validation_valid_defaults() {
1572        Settings::default().validate().unwrap();
1573        Settings::min_memory().validate().unwrap();
1574        Settings::high_performance().validate().unwrap();
1575    }
1576
1577    #[test]
1578    fn disk_config_from_settings() {
1579        let s = Settings::default();
1580        let dc = crate::disk::DiskConfig::from(&s);
1581        assert_eq!(dc.io_threads, default_disk_io_threads());
1582        assert_eq!(dc.storage_mode, StorageMode::Auto);
1583        assert_eq!(dc.cache_size, 16 * 1024 * 1024);
1584        assert!((dc.write_cache_ratio - 0.5).abs() < f32::EPSILON);
1585        assert_eq!(dc.channel_capacity, 512);
1586    }
1587
1588    #[test]
1589    fn torrent_config_from_settings() {
1590        let s = Settings::default();
1591        let tc = crate::types::TorrentConfig::from(&s);
1592        assert_eq!(tc.listen_port, 0); // random per-torrent
1593        assert_eq!(tc.max_peers, s.max_peers_per_torrent);
1594        assert_eq!(tc.download_dir, s.download_dir);
1595        assert_eq!(tc.enable_dht, s.enable_dht);
1596        assert_eq!(tc.enable_pex, s.enable_pex);
1597        assert_eq!(tc.encryption_mode, s.encryption_mode);
1598        assert_eq!(tc.enable_utp, s.enable_utp);
1599        assert_eq!(tc.enable_web_seed, s.enable_web_seed);
1600        assert_eq!(tc.hashing_threads, s.hashing_threads);
1601        assert_eq!(
1602            tc.max_concurrent_stream_reads,
1603            s.max_concurrent_stream_reads
1604        );
1605        assert_eq!(tc.anonymous_mode, s.anonymous_mode);
1606        assert_eq!(tc.enable_i2p, s.enable_i2p);
1607        assert_eq!(tc.allow_i2p_mixed, s.allow_i2p_mixed);
1608        // Previously hardcoded — now wired from Settings
1609        assert_eq!(tc.strict_end_game, s.strict_end_game);
1610        assert_eq!(tc.upload_rate_limit, s.upload_rate_limit);
1611        assert_eq!(tc.download_rate_limit, s.download_rate_limit);
1612        assert_eq!(tc.max_web_seeds, s.max_web_seeds);
1613        assert_eq!(tc.initial_picker_threshold, s.initial_picker_threshold);
1614        assert_eq!(tc.whole_pieces_threshold, s.whole_pieces_threshold);
1615        assert_eq!(tc.snub_timeout_secs, s.snub_timeout_secs);
1616        assert_eq!(tc.readahead_pieces, s.readahead_pieces);
1617        assert_eq!(
1618            tc.streaming_timeout_escalation,
1619            s.streaming_timeout_escalation
1620        );
1621        // New fields
1622        assert_eq!(tc.storage_mode, s.storage_mode);
1623        assert_eq!(tc.block_request_timeout_secs, s.block_request_timeout_secs);
1624        assert_eq!(tc.enable_lsd, s.enable_lsd);
1625        assert_eq!(tc.force_proxy, s.force_proxy);
1626        // M132: steal-queue population interval
1627        assert_eq!(tc.steal_stale_piece_secs, 2);
1628        assert_eq!(tc.steal_stale_piece_secs, s.steal_stale_piece_secs);
1629    }
1630
1631    #[test]
1632    fn torrent_config_from_nondefault_settings() {
1633        // Verify non-default values flow through (catches re-hardcoding regressions)
1634        let mut s = Settings::default();
1635        s.strict_end_game = false;
1636        s.upload_rate_limit = 1_000_000;
1637        s.download_rate_limit = 2_000_000;
1638        s.max_web_seeds = 8;
1639        s.initial_picker_threshold = 10;
1640        s.whole_pieces_threshold = 50;
1641        s.snub_timeout_secs = 120;
1642        s.readahead_pieces = 16;
1643        s.streaming_timeout_escalation = false;
1644        s.storage_mode = StorageMode::Full;
1645        s.block_request_timeout_secs = 30;
1646        s.enable_lsd = false;
1647        s.force_proxy = true;
1648        s.proxy.proxy_type = crate::proxy::ProxyType::Socks5;
1649
1650        let tc = crate::types::TorrentConfig::from(&s);
1651        assert!(!tc.strict_end_game);
1652        assert_eq!(tc.upload_rate_limit, 1_000_000);
1653        assert_eq!(tc.download_rate_limit, 2_000_000);
1654        assert_eq!(tc.max_web_seeds, 8);
1655        assert_eq!(tc.initial_picker_threshold, 10);
1656        assert_eq!(tc.whole_pieces_threshold, 50);
1657        assert_eq!(tc.snub_timeout_secs, 120);
1658        assert_eq!(tc.readahead_pieces, 16);
1659        assert!(!tc.streaming_timeout_escalation);
1660        assert_eq!(tc.storage_mode, StorageMode::Full);
1661        assert_eq!(tc.block_request_timeout_secs, 30);
1662        assert!(!tc.enable_lsd);
1663        assert!(tc.force_proxy);
1664    }
1665
1666    #[test]
1667    fn external_ip_default_and_json() {
1668        let s = Settings::default();
1669        assert!(s.external_ip.is_none());
1670
1671        // JSON with external_ip set
1672        let json = r#"{"external_ip": "203.0.113.5"}"#;
1673        let decoded: Settings = serde_json::from_str(json).unwrap();
1674        assert_eq!(
1675            decoded.external_ip,
1676            Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(
1677                203, 0, 113, 5
1678            )))
1679        );
1680
1681        // Round-trip preserves external_ip
1682        let encoded = serde_json::to_string(&decoded).unwrap();
1683        let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
1684        assert_eq!(roundtrip.external_ip, decoded.external_ip);
1685    }
1686
1687    #[test]
1688    fn validation_zero_threads() {
1689        let mut s = Settings::default();
1690        s.hashing_threads = 0;
1691        let err = s.validate().unwrap_err();
1692        assert!(err.to_string().contains("hashing_threads"));
1693
1694        let mut s = Settings::default();
1695        s.disk_io_threads = 0;
1696        let err = s.validate().unwrap_err();
1697        assert!(err.to_string().contains("disk_io_threads"));
1698
1699        let mut s = Settings::default();
1700        s.max_blocking_threads = 0;
1701        let err = s.validate().unwrap_err();
1702        assert!(err.to_string().contains("max_blocking_threads"));
1703    }
1704
1705    #[test]
1706    fn share_mode_requires_fast_extension() {
1707        let mut s = Settings::default();
1708        s.default_share_mode = true;
1709        s.enable_fast_extension = false;
1710        let err = s.validate().unwrap_err();
1711        assert!(err.to_string().contains("share_mode"));
1712
1713        // With fast extension enabled, share mode is valid
1714        s.enable_fast_extension = true;
1715        s.validate().unwrap();
1716    }
1717
1718    #[test]
1719    fn share_mode_default_false() {
1720        let cfg = crate::types::TorrentConfig::default();
1721        assert!(!cfg.share_mode);
1722    }
1723
1724    #[test]
1725    fn dht_storage_settings_defaults() {
1726        let s = Settings::default();
1727        assert_eq!(s.dht_max_items, 700);
1728        assert_eq!(s.dht_item_lifetime_secs, 7200);
1729    }
1730
1731    #[test]
1732    fn dht_sample_interval_default_disabled() {
1733        let s = Settings::default();
1734        assert_eq!(s.dht_sample_infohashes_interval, 0);
1735    }
1736
1737    #[test]
1738    fn dht_sample_interval_json_round_trip() {
1739        let json = r#"{"dht_sample_infohashes_interval": 300}"#;
1740        let decoded: Settings = serde_json::from_str(json).unwrap();
1741        assert_eq!(decoded.dht_sample_infohashes_interval, 300);
1742
1743        let encoded = serde_json::to_string(&decoded).unwrap();
1744        let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
1745        assert_eq!(roundtrip.dht_sample_infohashes_interval, 300);
1746    }
1747
1748    #[test]
1749    fn min_memory_restricts_dht_items() {
1750        let s = Settings::min_memory();
1751        assert_eq!(s.dht_max_items, 100);
1752    }
1753
1754    #[test]
1755    fn dht_config_inherits_security_settings() {
1756        let mut s = Settings::default();
1757        s.dht_enforce_node_id = false;
1758        let dht = s.to_dht_config();
1759        assert!(!dht.enforce_node_id);
1760        assert!(dht.restrict_routing_ips);
1761
1762        let dht_v6 = s.to_dht_config_v6();
1763        assert!(!dht_v6.enforce_node_id);
1764        assert!(dht_v6.restrict_routing_ips);
1765    }
1766
1767    #[test]
1768    fn enable_holepunch_default_true() {
1769        let s = Settings::default();
1770        assert!(s.enable_holepunch);
1771    }
1772
1773    #[test]
1774    fn enable_holepunch_json_round_trip() {
1775        let json = r#"{"enable_holepunch": false}"#;
1776        let decoded: Settings = serde_json::from_str(json).unwrap();
1777        assert!(!decoded.enable_holepunch);
1778
1779        let encoded = serde_json::to_string(&decoded).unwrap();
1780        let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
1781        assert!(!roundtrip.enable_holepunch);
1782    }
1783
1784    #[test]
1785    fn i2p_settings_defaults() {
1786        let s = Settings::default();
1787        assert!(!s.enable_i2p);
1788        assert_eq!(s.i2p_hostname, "127.0.0.1");
1789        assert_eq!(s.i2p_port, 7656);
1790        assert_eq!(s.i2p_inbound_quantity, 3);
1791        assert_eq!(s.i2p_outbound_quantity, 3);
1792        assert_eq!(s.i2p_inbound_length, 3);
1793        assert_eq!(s.i2p_outbound_length, 3);
1794        assert!(!s.allow_i2p_mixed);
1795    }
1796
1797    #[test]
1798    fn i2p_settings_json_roundtrip() {
1799        let mut s = Settings::default();
1800        s.enable_i2p = true;
1801        s.i2p_hostname = "10.0.0.1".into();
1802        s.i2p_port = 7700;
1803        s.i2p_inbound_quantity = 5;
1804        s.i2p_outbound_quantity = 4;
1805        s.i2p_inbound_length = 2;
1806        s.i2p_outbound_length = 1;
1807        s.allow_i2p_mixed = true;
1808        let json = serde_json::to_string(&s).unwrap();
1809        let decoded: Settings = serde_json::from_str(&json).unwrap();
1810        assert_eq!(s, decoded);
1811    }
1812
1813    #[test]
1814    fn i2p_validation_quantity_zero() {
1815        let mut s = Settings::default();
1816        s.enable_i2p = true;
1817        s.i2p_inbound_quantity = 0;
1818        let err = s.validate().unwrap_err();
1819        assert!(err.to_string().contains("i2p_inbound_quantity"));
1820    }
1821
1822    #[test]
1823    fn i2p_validation_quantity_too_high() {
1824        let mut s = Settings::default();
1825        s.enable_i2p = true;
1826        s.i2p_outbound_quantity = 17;
1827        let err = s.validate().unwrap_err();
1828        assert!(err.to_string().contains("i2p_outbound_quantity"));
1829    }
1830
1831    #[test]
1832    fn i2p_validation_length_too_high() {
1833        let mut s = Settings::default();
1834        s.enable_i2p = true;
1835        s.i2p_inbound_length = 8;
1836        let err = s.validate().unwrap_err();
1837        assert!(err.to_string().contains("i2p_inbound_length"));
1838    }
1839
1840    #[test]
1841    fn i2p_validation_passes_when_disabled() {
1842        // Invalid values should not trigger errors when I2P is disabled
1843        let mut s = Settings::default();
1844        s.enable_i2p = false;
1845        s.i2p_inbound_quantity = 0; // would be invalid if enabled
1846        s.validate().unwrap(); // should pass
1847    }
1848
1849    #[test]
1850    fn i2p_validation_valid_config() {
1851        let mut s = Settings::default();
1852        s.enable_i2p = true;
1853        s.i2p_inbound_quantity = 1;
1854        s.i2p_outbound_quantity = 16;
1855        s.i2p_inbound_length = 0;
1856        s.i2p_outbound_length = 7;
1857        s.validate().unwrap();
1858    }
1859
1860    #[test]
1861    fn ssl_settings_defaults() {
1862        let s = Settings::default();
1863        assert_eq!(s.ssl_listen_port, 0);
1864        assert!(s.ssl_cert_path.is_none());
1865        assert!(s.ssl_key_path.is_none());
1866    }
1867
1868    #[test]
1869    fn ssl_settings_json_round_trip() {
1870        let mut s = Settings::default();
1871        s.ssl_listen_port = 4433;
1872        s.ssl_cert_path = Some(PathBuf::from("/etc/ssl/cert.pem"));
1873        s.ssl_key_path = Some(PathBuf::from("/etc/ssl/key.pem"));
1874        let json = serde_json::to_string(&s).unwrap();
1875        let decoded: Settings = serde_json::from_str(&json).unwrap();
1876        assert_eq!(s, decoded);
1877    }
1878
1879    #[test]
1880    fn ssl_validation_cert_without_key() {
1881        let mut s = Settings::default();
1882        s.ssl_cert_path = Some(PathBuf::from("/tmp/cert.pem"));
1883        // ssl_key_path is None
1884        let err = s.validate().unwrap_err();
1885        assert!(err.to_string().contains("ssl_cert_path"));
1886    }
1887
1888    #[test]
1889    fn ssl_validation_key_without_cert() {
1890        let mut s = Settings::default();
1891        s.ssl_key_path = Some(PathBuf::from("/tmp/key.pem"));
1892        // ssl_cert_path is None
1893        let err = s.validate().unwrap_err();
1894        assert!(err.to_string().contains("ssl_cert_path"));
1895    }
1896
1897    #[test]
1898    fn ssl_validation_both_set_passes() {
1899        let mut s = Settings::default();
1900        s.ssl_cert_path = Some(PathBuf::from("/tmp/cert.pem"));
1901        s.ssl_key_path = Some(PathBuf::from("/tmp/key.pem"));
1902        s.validate().unwrap();
1903    }
1904
1905    #[test]
1906    fn ssl_validation_both_absent_passes() {
1907        let s = Settings::default();
1908        // Both are None by default
1909        s.validate().unwrap();
1910    }
1911
1912    #[test]
1913    fn default_choking_algorithms() {
1914        let s = Settings::default();
1915        assert_eq!(
1916            s.seed_choking_algorithm,
1917            SeedChokingAlgorithm::FastestUpload
1918        );
1919        assert_eq!(s.choking_algorithm, ChokingAlgorithm::FixedSlots);
1920    }
1921
1922    #[test]
1923    fn choking_algorithm_json_round_trip() {
1924        let mut s = Settings::default();
1925        s.seed_choking_algorithm = SeedChokingAlgorithm::AntiLeech;
1926        s.choking_algorithm = ChokingAlgorithm::RateBased;
1927        let json = serde_json::to_string(&s).unwrap();
1928        let decoded: Settings = serde_json::from_str(&json).unwrap();
1929        assert_eq!(
1930            decoded.seed_choking_algorithm,
1931            SeedChokingAlgorithm::AntiLeech
1932        );
1933        assert_eq!(decoded.choking_algorithm, ChokingAlgorithm::RateBased);
1934    }
1935
1936    #[test]
1937    fn m44_settings_defaults() {
1938        let s = Settings::default();
1939        assert!(s.piece_extent_affinity);
1940        assert!(!s.suggest_mode);
1941        assert_eq!(s.max_suggest_pieces, 16);
1942        assert_eq!(s.predictive_piece_announce_ms, 0);
1943    }
1944
1945    #[test]
1946    fn m44_high_performance_enables_suggest() {
1947        let s = Settings::high_performance();
1948        assert!(s.suggest_mode);
1949    }
1950
1951    #[test]
1952    fn m44_json_round_trip() {
1953        let mut s = Settings::default();
1954        s.piece_extent_affinity = false;
1955        s.suggest_mode = true;
1956        s.max_suggest_pieces = 5;
1957        s.predictive_piece_announce_ms = 50;
1958        let json = serde_json::to_string(&s).unwrap();
1959        let decoded: Settings = serde_json::from_str(&json).unwrap();
1960        assert_eq!(s, decoded);
1961    }
1962
1963    #[test]
1964    fn security_settings_defaults() {
1965        let s = Settings::default();
1966        assert!(s.ssrf_mitigation);
1967        assert!(!s.allow_idna);
1968        assert!(s.validate_https_trackers);
1969    }
1970
1971    #[test]
1972    fn security_settings_json_round_trip() {
1973        let mut s = Settings::default();
1974        s.ssrf_mitigation = false;
1975        s.allow_idna = true;
1976        s.validate_https_trackers = false;
1977        let json = serde_json::to_string(&s).unwrap();
1978        let decoded: Settings = serde_json::from_str(&json).unwrap();
1979        assert_eq!(s, decoded);
1980    }
1981
1982    #[test]
1983    fn security_settings_missing_use_defaults() {
1984        // An empty JSON object should deserialize security fields to defaults.
1985        let decoded: Settings = serde_json::from_str("{}").unwrap();
1986        assert!(decoded.ssrf_mitigation);
1987        assert!(!decoded.allow_idna);
1988        assert!(decoded.validate_https_trackers);
1989    }
1990
1991    #[test]
1992    fn url_security_config_from_settings() {
1993        let mut s = Settings::default();
1994        s.ssrf_mitigation = false;
1995        s.allow_idna = true;
1996        s.validate_https_trackers = false;
1997        let cfg = crate::url_guard::UrlSecurityConfig::from(&s);
1998        assert!(!cfg.ssrf_mitigation);
1999        assert!(cfg.allow_idna);
2000        assert!(!cfg.validate_https_trackers);
2001    }
2002
2003    #[test]
2004    fn default_peer_dscp_value() {
2005        let s = Settings::default();
2006        assert_eq!(s.peer_dscp, 0x08);
2007    }
2008
2009    #[test]
2010    fn peer_dscp_json_round_trip() {
2011        let mut s = Settings::default();
2012        s.peer_dscp = 0x2E; // EF
2013        let json = serde_json::to_string(&s).unwrap();
2014        let decoded: Settings = serde_json::from_str(&json).unwrap();
2015        assert_eq!(decoded.peer_dscp, 0x2E);
2016    }
2017
2018    #[test]
2019    fn peer_dscp_zero_disables() {
2020        let mut s = Settings::default();
2021        s.peer_dscp = 0;
2022        let json = serde_json::to_string(&s).unwrap();
2023        let decoded: Settings = serde_json::from_str(&json).unwrap();
2024        assert_eq!(decoded.peer_dscp, 0);
2025    }
2026
2027    #[test]
2028    fn utp_config_includes_dscp() {
2029        let mut s = Settings::default();
2030        s.peer_dscp = 0x0A;
2031        let utp = s.to_utp_config(6881);
2032        assert_eq!(utp.dscp, 0x0A);
2033
2034        let utp_v6 = s.to_utp_config_v6(6881);
2035        assert_eq!(utp_v6.dscp, 0x0A);
2036    }
2037
2038    #[test]
2039    fn default_stats_report_interval() {
2040        let s = Settings::default();
2041        assert_eq!(s.stats_report_interval, 1000);
2042    }
2043
2044    #[test]
2045    fn stats_report_interval_json_round_trip() {
2046        let mut s = Settings::default();
2047        s.stats_report_interval = 5000;
2048        let json = serde_json::to_string(&s).unwrap();
2049        let decoded: Settings = serde_json::from_str(&json).unwrap();
2050        assert_eq!(decoded.stats_report_interval, 5000);
2051    }
2052
2053    #[test]
2054    fn stats_report_interval_zero_disables() {
2055        let mut s = Settings::default();
2056        s.stats_report_interval = 0;
2057        let json = serde_json::to_string(&s).unwrap();
2058        let decoded: Settings = serde_json::from_str(&json).unwrap();
2059        assert_eq!(decoded.stats_report_interval, 0);
2060    }
2061
2062    #[test]
2063    fn settings_runtime_worker_threads_and_pin_cores() {
2064        // Defaults
2065        let s = Settings::default();
2066        assert_eq!(s.runtime_worker_threads, default_runtime_worker_threads());
2067        assert!(s.pin_cores);
2068
2069        // 0 is valid (means auto-detect)
2070        let mut s = Settings::default();
2071        s.runtime_worker_threads = 0;
2072        assert!(s.validate().is_ok());
2073
2074        // 256 is valid (boundary)
2075        s.runtime_worker_threads = 256;
2076        assert!(s.validate().is_ok());
2077
2078        // 257 is invalid
2079        s.runtime_worker_threads = 257;
2080        assert!(s.validate().is_err());
2081    }
2082
2083    #[test]
2084    fn max_in_flight_512_default() {
2085        let s = Settings::default();
2086        assert_eq!(s.max_in_flight_pieces, 512);
2087        assert_eq!(s.fixed_pipeline_depth, 128);
2088
2089        // Presets
2090        let mm = Settings::min_memory();
2091        assert_eq!(mm.max_in_flight_pieces, 32);
2092        assert_eq!(mm.fixed_pipeline_depth, 32);
2093
2094        let hp = Settings::high_performance();
2095        assert_eq!(hp.max_in_flight_pieces, 512);
2096        assert_eq!(hp.fixed_pipeline_depth, 128); // inherits default
2097    }
2098
2099    #[test]
2100    fn recalc_max_in_flight_formula() {
2101        // M104: The formula in torrent.rs: max(512, connected * 4), clamped to
2102        // num_pieces / 2, floored at 512. Validate the logic here.
2103        let base = 512_usize;
2104
2105        // Few peers: floor dominates
2106        let connected = 10;
2107        let num_pieces = 2000_u32;
2108        let calculated = base.max(connected * 4);
2109        let result = calculated.min(num_pieces as usize / 2).max(base);
2110        assert_eq!(result, 512); // max(512, 40) = 512, min(512, 1000) = 512
2111
2112        // Many peers: peer count drives it up
2113        let connected = 200;
2114        let calculated = base.max(connected * 4);
2115        let result = calculated.min(num_pieces as usize / 2).max(base);
2116        assert_eq!(result, 800); // max(512, 800) = 800, min(800, 1000) = 800
2117
2118        // Small torrent: piece clamp wins
2119        let connected = 200;
2120        let num_pieces = 100_u32;
2121        let calculated = base.max(connected * 4);
2122        let result = calculated.min(num_pieces as usize / 2).max(base);
2123        assert_eq!(result, 512); // max(512, 800) = 800, min(800, 50) = 50, max(50, 512) = 512
2124
2125        // Exact boundary: connected * 4 == base
2126        let connected = 129; // 129 * 4 = 516, just above 512
2127        let num_pieces = 10000_u32;
2128        let calculated = base.max(connected * 4);
2129        let result = calculated.min(num_pieces as usize / 2).max(base);
2130        assert_eq!(result, 516); // max(512, 516) = 516, min(516, 5000) = 516
2131    }
2132}