use std::net::IpAddr;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use irontide_core::StorageMode;
use irontide_wire::mse::EncryptionMode;
use crate::alert::AlertCategory;
use crate::choker::{ChokingAlgorithm, SeedChokingAlgorithm};
use crate::proxy::ProxyConfig;
use crate::rate_limiter::MixedModeAlgorithm;
fn default_true() -> bool {
true
}
fn default_listen_port() -> u16 {
42020
}
fn default_download_dir() -> PathBuf {
PathBuf::from(".")
}
fn default_max_torrents() -> usize {
100
}
fn default_encryption() -> EncryptionMode {
EncryptionMode::Disabled
}
fn default_auto_upload_slots_min() -> usize {
2
}
fn default_auto_upload_slots_max() -> usize {
20
}
fn default_active_downloads() -> i32 {
3
}
fn default_active_seeds() -> i32 {
5
}
fn default_active_limit() -> i32 {
500
}
fn default_active_checking() -> i32 {
1
}
fn default_inactive_rate() -> u64 {
2048
}
fn default_auto_manage_interval() -> u64 {
30
}
fn default_auto_manage_startup() -> u64 {
60
}
fn default_alert_mask() -> AlertCategory {
AlertCategory::ALL
}
fn default_alert_channel_size() -> usize {
1024
}
fn default_smart_ban_max_failures() -> u32 {
3
}
fn default_disk_io_threads() -> usize {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
(cores / 2).clamp(4, 16)
}
fn default_max_blocking_threads() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
}
fn default_storage_mode() -> StorageMode {
StorageMode::Auto
}
fn default_disk_cache_size() -> usize {
16 * 1024 * 1024
}
fn default_disk_write_cache_ratio() -> f32 {
0.5
}
fn default_buffer_pool_capacity() -> usize {
64 * 1024 * 1024
}
fn default_enable_mlock() -> bool {
cfg!(unix)
}
fn default_io_uring_sq_depth() -> u32 {
256
}
fn default_io_uring_batch_threshold() -> usize {
4
}
fn default_disk_channel_capacity() -> usize {
512
}
fn default_hashing_threads() -> usize {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
(cores / 4).clamp(2, 8)
}
fn default_max_request_queue_depth() -> usize {
250
}
fn default_initial_queue_depth() -> usize {
128
}
fn default_request_queue_time() -> f64 {
3.0
}
fn default_block_request_timeout() -> u32 {
60
}
fn default_max_concurrent_streams() -> usize {
8
}
fn default_dht_qps() -> usize {
50
}
fn default_dht_timeout() -> u64 {
5
}
fn default_upnp_lease() -> u32 {
3600
}
fn default_natpmp_lifetime() -> u32 {
7200
}
fn default_utp_max_conns() -> usize {
256
}
fn default_dht_max_items() -> usize {
700
}
fn default_dht_item_lifetime() -> u64 {
7200
}
fn default_dht_sample_interval() -> u64 {
0
}
fn default_max_suggest_pieces() -> usize {
16
}
fn default_predictive_piece_announce_ms() -> u64 {
0
}
fn default_ssl_listen_port() -> u16 {
0 }
fn default_seed_choking_algorithm() -> SeedChokingAlgorithm {
SeedChokingAlgorithm::FastestUpload
}
fn default_choking_algorithm() -> ChokingAlgorithm {
ChokingAlgorithm::FixedSlots
}
fn default_mixed_mode() -> MixedModeAlgorithm {
MixedModeAlgorithm::PeerProportional
}
fn default_steal_threshold_ratio() -> f64 {
10.0
}
fn default_use_block_stealing() -> bool {
true
}
fn default_peer_connect_timeout() -> u64 {
10 }
fn default_peer_dscp() -> u8 {
0x08 }
fn default_max_peers_per_torrent() -> usize {
128
}
fn default_stats_report_interval() -> u64 {
1000
}
fn default_strict_end_game() -> bool {
true
}
fn default_max_web_seeds() -> usize {
4
}
fn default_initial_picker_threshold() -> u32 {
4
}
fn default_whole_pieces_threshold() -> u32 {
20
}
fn default_snub_timeout_secs() -> u32 {
15
}
fn default_readahead_pieces() -> u32 {
8
}
fn default_max_metadata_size() -> u64 {
4 * 1024 * 1024 }
fn default_max_message_size() -> usize {
16 * 1024 * 1024 }
fn default_max_piece_length() -> u64 {
32 * 1024 * 1024 }
fn default_max_outstanding_requests() -> usize {
500
}
fn default_max_in_flight_pieces() -> usize {
512
}
fn default_fixed_pipeline_depth() -> usize {
128
}
fn default_i2p_hostname() -> String {
"127.0.0.1".into()
}
fn default_i2p_port() -> u16 {
7656
}
fn default_i2p_tunnel_quantity() -> u8 {
3
}
fn default_i2p_tunnel_length() -> u8 {
3
}
fn default_runtime_worker_threads() -> usize {
std::thread::available_parallelism()
.map(|n| n.get().min(8))
.unwrap_or(4)
}
fn default_lock_warn_threshold_ms() -> u64 {
50
}
fn default_steal_stale_piece_secs() -> u64 {
2
}
fn default_steal_threshold_endgame() -> f64 {
3.0
}
fn default_min_pipeline_depth() -> u32 {
16
}
fn default_max_pipeline_depth() -> u32 {
512
}
fn default_target_buffer_secs() -> f64 {
2.0
}
fn default_peer_read_timeout_secs() -> u64 {
10
}
fn default_peer_write_timeout_secs() -> u64 {
10
}
fn default_data_contribution_timeout() -> u64 {
0 }
fn default_choke_rotation_max_evictions() -> u32 {
0 }
fn default_max_concurrent_connects() -> u16 {
128 }
fn default_connect_soft_timeout() -> u64 {
3 }
fn default_save_resume_interval() -> u64 {
300 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(default = "default_listen_port")]
pub listen_port: u16,
#[serde(default = "default_download_dir")]
pub download_dir: PathBuf,
#[serde(default = "default_max_torrents")]
pub max_torrents: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume_data_dir: Option<PathBuf>,
#[serde(default = "default_save_resume_interval")]
pub save_resume_interval_secs: u64,
#[serde(default = "default_true")]
pub enable_dht: bool,
#[serde(default = "default_true")]
pub enable_pex: bool,
#[serde(default = "default_true")]
pub enable_lsd: bool,
#[serde(default = "default_true")]
pub enable_fast_extension: bool,
#[serde(default = "default_true")]
pub enable_utp: bool,
#[serde(default = "default_true")]
pub enable_upnp: bool,
#[serde(default = "default_true")]
pub enable_natpmp: bool,
#[serde(default = "default_true")]
pub enable_ipv6: bool,
#[serde(default = "default_true")]
pub enable_web_seed: bool,
#[serde(default = "default_true")]
pub enable_holepunch: bool,
#[serde(default = "default_true")]
pub enable_bep40_eviction: bool,
#[serde(default = "default_encryption")]
pub encryption_mode: EncryptionMode,
#[serde(default)]
pub anonymous_mode: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external_ip: Option<IpAddr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed_ratio_limit: Option<f64>,
#[serde(default)]
pub default_super_seeding: bool,
#[serde(default)]
pub default_share_mode: bool,
#[serde(default = "default_true")]
pub upload_only_announce: bool,
#[serde(default)]
pub upload_rate_limit: u64,
#[serde(default)]
pub download_rate_limit: u64,
#[serde(default)]
pub tcp_upload_rate_limit: u64,
#[serde(default)]
pub tcp_download_rate_limit: u64,
#[serde(default)]
pub utp_upload_rate_limit: u64,
#[serde(default)]
pub utp_download_rate_limit: u64,
#[serde(default = "default_true")]
pub auto_upload_slots: bool,
#[serde(default = "default_auto_upload_slots_min")]
pub auto_upload_slots_min: usize,
#[serde(default = "default_auto_upload_slots_max")]
pub auto_upload_slots_max: usize,
#[serde(default = "default_mixed_mode")]
pub mixed_mode_algorithm: MixedModeAlgorithm,
#[serde(default = "default_active_downloads")]
pub active_downloads: i32,
#[serde(default = "default_active_seeds")]
pub active_seeds: i32,
#[serde(default = "default_active_limit")]
pub active_limit: i32,
#[serde(default = "default_active_checking")]
pub active_checking: i32,
#[serde(default = "default_true")]
pub dont_count_slow_torrents: bool,
#[serde(default = "default_inactive_rate")]
pub inactive_down_rate: u64,
#[serde(default = "default_inactive_rate")]
pub inactive_up_rate: u64,
#[serde(default = "default_auto_manage_interval")]
pub auto_manage_interval: u64,
#[serde(default = "default_auto_manage_startup")]
pub auto_manage_startup: u64,
#[serde(default)]
pub auto_manage_prefer_seeds: bool,
#[serde(default = "default_alert_mask")]
pub alert_mask: AlertCategory,
#[serde(default = "default_alert_channel_size")]
pub alert_channel_size: usize,
#[serde(default = "default_smart_ban_max_failures")]
pub smart_ban_max_failures: u32,
#[serde(default = "default_true")]
pub smart_ban_parole: bool,
#[serde(default = "default_disk_io_threads")]
pub disk_io_threads: usize,
#[serde(default = "default_max_blocking_threads")]
pub max_blocking_threads: usize,
#[serde(default = "default_storage_mode")]
pub storage_mode: StorageMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preallocate_mode: Option<irontide_storage::PreallocateMode>,
#[serde(default = "default_disk_cache_size")]
pub disk_cache_size: usize,
#[serde(default = "default_disk_write_cache_ratio")]
pub disk_write_cache_ratio: f32,
#[serde(default = "default_disk_channel_capacity")]
pub disk_channel_capacity: usize,
#[serde(default = "default_buffer_pool_capacity")]
pub buffer_pool_capacity: usize,
#[serde(default = "default_enable_mlock")]
pub enable_mlock: bool,
#[serde(default = "default_io_uring_sq_depth")]
pub io_uring_sq_depth: u32,
#[serde(default)]
pub io_uring_direct_io: bool,
#[serde(default)]
pub filesystem_direct_io: bool,
#[serde(default = "default_io_uring_batch_threshold")]
pub io_uring_batch_threshold: usize,
#[serde(default)]
pub iocp_concurrent_threads: u32,
#[serde(default)]
pub iocp_direct_io: bool,
#[serde(default = "default_hashing_threads")]
pub hashing_threads: usize,
#[serde(default = "default_max_request_queue_depth")]
pub max_request_queue_depth: usize,
#[serde(default = "default_initial_queue_depth")]
pub initial_queue_depth: usize,
#[serde(default = "default_request_queue_time")]
pub request_queue_time: f64,
#[serde(default = "default_block_request_timeout")]
pub block_request_timeout_secs: u32,
#[serde(default = "default_max_concurrent_streams")]
pub max_concurrent_stream_reads: usize,
#[serde(default = "default_true")]
pub auto_sequential: bool,
#[serde(default = "default_strict_end_game")]
pub strict_end_game: bool,
#[serde(default = "default_max_web_seeds")]
pub max_web_seeds: usize,
#[serde(default = "default_initial_picker_threshold")]
pub initial_picker_threshold: u32,
#[serde(default = "default_whole_pieces_threshold")]
pub whole_pieces_threshold: u32,
#[serde(default = "default_snub_timeout_secs")]
pub snub_timeout_secs: u32,
#[serde(default = "default_readahead_pieces")]
pub readahead_pieces: u32,
#[serde(default = "default_true")]
pub streaming_timeout_escalation: bool,
#[serde(default = "default_steal_threshold_ratio")]
pub steal_threshold_ratio: f64,
#[serde(default = "default_use_block_stealing")]
pub use_block_stealing: bool,
#[serde(default = "default_steal_stale_piece_secs")]
pub steal_stale_piece_secs: u64,
#[serde(default = "default_steal_threshold_endgame")]
pub steal_threshold_endgame: f64,
#[serde(default = "default_min_pipeline_depth")]
pub min_pipeline_depth: u32,
#[serde(default = "default_max_pipeline_depth")]
pub max_pipeline_depth: u32,
#[serde(default = "default_target_buffer_secs")]
pub target_buffer_secs: f64,
#[serde(default = "default_fixed_pipeline_depth")]
pub fixed_pipeline_depth: usize,
#[serde(default = "default_true")]
pub piece_extent_affinity: bool,
#[serde(default)]
pub suggest_mode: bool,
#[serde(default = "default_max_suggest_pieces")]
pub max_suggest_pieces: usize,
#[serde(default = "default_predictive_piece_announce_ms")]
pub predictive_piece_announce_ms: u64,
#[serde(default)]
pub proxy: ProxyConfig,
#[serde(default)]
pub force_proxy: bool,
#[serde(default = "default_true")]
pub apply_ip_filter_to_trackers: bool,
#[serde(default = "default_dht_qps")]
pub dht_queries_per_second: usize,
#[serde(default = "default_dht_timeout")]
pub dht_query_timeout_secs: u64,
#[serde(default)]
pub dht_enforce_node_id: bool,
#[serde(default = "default_true")]
pub dht_restrict_routing_ips: bool,
#[serde(default = "default_dht_max_items")]
pub dht_max_items: usize,
#[serde(default = "default_dht_item_lifetime")]
pub dht_item_lifetime_secs: u64,
#[serde(default = "default_dht_sample_interval")]
pub dht_sample_infohashes_interval: u64,
#[serde(default)]
pub dht_read_only: bool,
#[serde(default = "default_upnp_lease")]
pub upnp_lease_duration: u32,
#[serde(default = "default_natpmp_lifetime")]
pub natpmp_lifetime: u32,
#[serde(default = "default_utp_max_conns")]
pub utp_max_connections: usize,
#[serde(default)]
pub enable_i2p: bool,
#[serde(default = "default_i2p_hostname")]
pub i2p_hostname: String,
#[serde(default = "default_i2p_port")]
pub i2p_port: u16,
#[serde(default = "default_i2p_tunnel_quantity")]
pub i2p_inbound_quantity: u8,
#[serde(default = "default_i2p_tunnel_quantity")]
pub i2p_outbound_quantity: u8,
#[serde(default = "default_i2p_tunnel_length")]
pub i2p_inbound_length: u8,
#[serde(default = "default_i2p_tunnel_length")]
pub i2p_outbound_length: u8,
#[serde(default)]
pub allow_i2p_mixed: bool,
#[serde(default = "default_ssl_listen_port")]
pub ssl_listen_port: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ssl_cert_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ssl_key_path: Option<PathBuf>,
#[serde(default = "default_seed_choking_algorithm")]
pub seed_choking_algorithm: SeedChokingAlgorithm,
#[serde(default = "default_choking_algorithm")]
pub choking_algorithm: ChokingAlgorithm,
#[serde(default = "default_max_peers_per_torrent")]
pub max_peers_per_torrent: usize,
#[serde(default = "default_peer_read_timeout_secs")]
pub peer_read_timeout_secs: u64,
#[serde(default = "default_peer_write_timeout_secs")]
pub peer_write_timeout_secs: u64,
#[serde(default = "default_data_contribution_timeout")]
pub data_contribution_timeout_secs: u64,
#[serde(default = "default_choke_rotation_max_evictions")]
pub choke_rotation_max_evictions: u32,
#[serde(default = "default_max_concurrent_connects")]
pub max_concurrent_connects: u16,
#[serde(default = "default_connect_soft_timeout")]
pub connect_soft_timeout: u64,
#[serde(default = "default_true")]
pub ssrf_mitigation: bool,
#[serde(default)]
pub allow_idna: bool,
#[serde(default = "default_true")]
pub validate_https_trackers: bool,
#[serde(default = "default_max_metadata_size")]
pub max_metadata_size: u64,
#[serde(default = "default_max_message_size")]
pub max_message_size: usize,
#[serde(default = "default_max_piece_length")]
pub max_piece_length: u64,
#[serde(default = "default_max_outstanding_requests")]
pub max_outstanding_requests: usize,
#[serde(default = "default_max_in_flight_pieces")]
pub max_in_flight_pieces: usize,
#[serde(default = "default_peer_connect_timeout")]
pub peer_connect_timeout: u64,
#[serde(default = "default_peer_dscp")]
pub peer_dscp: u8,
#[serde(default = "default_stats_report_interval")]
pub stats_report_interval: u64,
#[serde(default = "default_runtime_worker_threads")]
pub runtime_worker_threads: usize,
#[serde(default = "default_true")]
pub pin_cores: bool,
#[serde(default = "default_lock_warn_threshold_ms")]
pub lock_warn_threshold_ms: u64,
#[serde(skip)]
pub dht_saved_nodes: Vec<String>,
#[serde(skip)]
pub dht_node_id: Option<irontide_core::Id20>,
}
impl Default for Settings {
fn default() -> Self {
Self {
listen_port: 42020,
download_dir: PathBuf::from("."),
max_torrents: 100,
resume_data_dir: None,
save_resume_interval_secs: 300,
enable_dht: true,
enable_pex: true,
enable_lsd: true,
enable_fast_extension: true,
enable_utp: true,
enable_upnp: true,
enable_natpmp: true,
enable_ipv6: true,
enable_web_seed: true,
enable_holepunch: true,
enable_bep40_eviction: true,
encryption_mode: EncryptionMode::Disabled,
anonymous_mode: false,
external_ip: None,
seed_ratio_limit: None,
default_super_seeding: false,
default_share_mode: false,
upload_only_announce: true,
upload_rate_limit: 0,
download_rate_limit: 0,
tcp_upload_rate_limit: 0,
tcp_download_rate_limit: 0,
utp_upload_rate_limit: 0,
utp_download_rate_limit: 0,
auto_upload_slots: true,
auto_upload_slots_min: 2,
auto_upload_slots_max: 20,
mixed_mode_algorithm: MixedModeAlgorithm::PeerProportional,
active_downloads: 3,
active_seeds: 5,
active_limit: 500,
active_checking: 1,
dont_count_slow_torrents: true,
inactive_down_rate: 2048,
inactive_up_rate: 2048,
auto_manage_interval: 30,
auto_manage_startup: 60,
auto_manage_prefer_seeds: false,
alert_mask: AlertCategory::ALL,
alert_channel_size: 1024,
smart_ban_max_failures: 3,
smart_ban_parole: true,
disk_io_threads: default_disk_io_threads(),
max_blocking_threads: default_max_blocking_threads(),
storage_mode: StorageMode::Auto,
preallocate_mode: None,
disk_cache_size: 16 * 1024 * 1024,
disk_write_cache_ratio: 0.5,
disk_channel_capacity: 512,
buffer_pool_capacity: 64 * 1024 * 1024,
enable_mlock: cfg!(unix),
io_uring_sq_depth: 256,
io_uring_direct_io: false,
filesystem_direct_io: false,
io_uring_batch_threshold: 4,
iocp_concurrent_threads: 0,
iocp_direct_io: false,
hashing_threads: default_hashing_threads(),
max_request_queue_depth: 250,
initial_queue_depth: 128,
request_queue_time: 3.0,
block_request_timeout_secs: 60,
max_concurrent_stream_reads: 8,
auto_sequential: true,
steal_threshold_ratio: 10.0,
use_block_stealing: true,
steal_stale_piece_secs: 2,
steal_threshold_endgame: 3.0,
min_pipeline_depth: 16,
max_pipeline_depth: 512,
target_buffer_secs: 2.0,
fixed_pipeline_depth: 128,
strict_end_game: true,
max_web_seeds: 4,
initial_picker_threshold: 4,
whole_pieces_threshold: 20,
snub_timeout_secs: 15,
readahead_pieces: 8,
streaming_timeout_escalation: true,
piece_extent_affinity: true,
suggest_mode: false,
max_suggest_pieces: 16,
predictive_piece_announce_ms: 0,
proxy: ProxyConfig::default(),
force_proxy: false,
apply_ip_filter_to_trackers: true,
dht_queries_per_second: 50,
dht_query_timeout_secs: 5,
dht_enforce_node_id: false,
dht_restrict_routing_ips: true,
dht_max_items: 700,
dht_item_lifetime_secs: 7200,
dht_sample_infohashes_interval: 0,
dht_read_only: false,
upnp_lease_duration: 3600,
natpmp_lifetime: 7200,
utp_max_connections: 256,
enable_i2p: false,
i2p_hostname: "127.0.0.1".into(),
i2p_port: 7656,
i2p_inbound_quantity: 3,
i2p_outbound_quantity: 3,
i2p_inbound_length: 3,
i2p_outbound_length: 3,
allow_i2p_mixed: false,
ssl_listen_port: 0,
ssl_cert_path: None,
ssl_key_path: None,
seed_choking_algorithm: SeedChokingAlgorithm::FastestUpload,
choking_algorithm: ChokingAlgorithm::FixedSlots,
max_peers_per_torrent: 128,
peer_read_timeout_secs: 10,
peer_write_timeout_secs: 10,
data_contribution_timeout_secs: 0,
choke_rotation_max_evictions: 0,
max_concurrent_connects: 128,
connect_soft_timeout: 3,
ssrf_mitigation: true,
allow_idna: false,
validate_https_trackers: true,
max_metadata_size: 4 * 1024 * 1024,
max_message_size: 16 * 1024 * 1024,
max_piece_length: 32 * 1024 * 1024,
max_outstanding_requests: 500,
max_in_flight_pieces: 512,
peer_connect_timeout: 10,
peer_dscp: 0x08,
stats_report_interval: 1000,
runtime_worker_threads: default_runtime_worker_threads(),
pin_cores: true,
lock_warn_threshold_ms: 50,
dht_saved_nodes: Vec::new(),
dht_node_id: None,
}
}
}
impl Settings {
pub fn min_memory() -> Self {
Self {
disk_cache_size: 8 * 1024 * 1024,
buffer_pool_capacity: 16 * 1024 * 1024,
max_torrents: 20,
max_peers_per_torrent: 30,
active_downloads: 1,
active_seeds: 2,
active_limit: 10,
alert_channel_size: 256,
utp_max_connections: 64,
max_request_queue_depth: 50,
initial_queue_depth: 16,
max_concurrent_stream_reads: 2,
hashing_threads: 1,
disk_io_threads: 1,
dht_max_items: 100,
max_in_flight_pieces: 32,
fixed_pipeline_depth: 32,
..Self::default()
}
}
pub fn high_performance() -> Self {
Self {
disk_cache_size: 256 * 1024 * 1024,
buffer_pool_capacity: 256 * 1024 * 1024,
max_torrents: 2000,
max_peers_per_torrent: 200,
active_downloads: 30,
active_seeds: 100,
active_limit: 2000,
alert_channel_size: 4096,
utp_max_connections: 1024,
max_request_queue_depth: 1000,
initial_queue_depth: 256,
max_concurrent_stream_reads: 32,
hashing_threads: 4,
disk_io_threads: 8,
auto_upload_slots_max: 100,
suggest_mode: true,
steal_threshold_ratio: 5.0,
steal_threshold_endgame: 2.0,
min_pipeline_depth: 16,
max_pipeline_depth: 512,
target_buffer_secs: 2.0,
use_block_stealing: true,
max_in_flight_pieces: 512,
..Self::default()
}
}
pub fn validate(&self) -> crate::Result<()> {
use crate::proxy::ProxyType;
if self.force_proxy && self.proxy.proxy_type == ProxyType::None {
return Err(crate::Error::InvalidSettings(
"force_proxy is enabled but no proxy type is configured".into(),
));
}
if self.active_downloads > 0
&& self.active_limit > 0
&& self.active_downloads > self.active_limit
{
return Err(crate::Error::InvalidSettings(
"active_downloads exceeds active_limit".into(),
));
}
if self.active_seeds > 0 && self.active_limit > 0 && self.active_seeds > self.active_limit {
return Err(crate::Error::InvalidSettings(
"active_seeds exceeds active_limit".into(),
));
}
if !(0.0..=1.0).contains(&self.disk_write_cache_ratio) {
return Err(crate::Error::InvalidSettings(
"disk_write_cache_ratio must be between 0.0 and 1.0".into(),
));
}
if self.disk_cache_size < 1024 * 1024 {
return Err(crate::Error::InvalidSettings(
"disk_cache_size must be at least 1 MiB".into(),
));
}
if self.hashing_threads == 0 {
return Err(crate::Error::InvalidSettings(
"hashing_threads must be at least 1".into(),
));
}
if self.disk_io_threads == 0 {
return Err(crate::Error::InvalidSettings(
"disk_io_threads must be at least 1".into(),
));
}
if self.max_blocking_threads == 0 {
return Err(crate::Error::InvalidSettings(
"max_blocking_threads must be at least 1".into(),
));
}
if self.default_share_mode && !self.enable_fast_extension {
return Err(crate::Error::InvalidSettings(
"share_mode requires enable_fast_extension for RejectRequest messages".into(),
));
}
if self.ssl_cert_path.is_some() != self.ssl_key_path.is_some() {
return Err(crate::Error::InvalidSettings(
"ssl_cert_path and ssl_key_path must both be set or both absent".into(),
));
}
if self.enable_i2p {
if self.i2p_inbound_quantity == 0 || self.i2p_inbound_quantity > 16 {
return Err(crate::Error::InvalidSettings(
"i2p_inbound_quantity must be 1-16".into(),
));
}
if self.i2p_outbound_quantity == 0 || self.i2p_outbound_quantity > 16 {
return Err(crate::Error::InvalidSettings(
"i2p_outbound_quantity must be 1-16".into(),
));
}
if self.i2p_inbound_length > 7 {
return Err(crate::Error::InvalidSettings(
"i2p_inbound_length must be 0-7".into(),
));
}
if self.i2p_outbound_length > 7 {
return Err(crate::Error::InvalidSettings(
"i2p_outbound_length must be 0-7".into(),
));
}
}
if self.runtime_worker_threads > 256 {
return Err(crate::Error::InvalidSettings(
"runtime_worker_threads must be at most 256".into(),
));
}
Ok(())
}
}
impl From<&Settings> for crate::disk::DiskConfig {
fn from(s: &Settings) -> Self {
Self {
io_threads: s.disk_io_threads,
storage_mode: s.storage_mode,
cache_size: s.disk_cache_size,
write_cache_ratio: s.disk_write_cache_ratio,
channel_capacity: s.disk_channel_capacity,
buffer_pool_capacity: s.buffer_pool_capacity,
enable_mlock: s.enable_mlock,
lock_warn_threshold_ms: s.lock_warn_threshold_ms,
io_uring_sq_depth: s.io_uring_sq_depth,
io_uring_direct_io: s.io_uring_direct_io,
filesystem_direct_io: s.filesystem_direct_io,
io_uring_batch_threshold: s.io_uring_batch_threshold,
iocp_concurrent_threads: s.iocp_concurrent_threads,
iocp_direct_io: s.iocp_direct_io,
}
}
}
impl From<&Settings> for crate::ban::BanConfig {
fn from(s: &Settings) -> Self {
Self {
max_failures: s.smart_ban_max_failures,
use_parole: s.smart_ban_parole,
}
}
}
impl Settings {
pub(crate) fn to_dht_config(&self) -> irontide_dht::DhtConfig {
let default = irontide_dht::DhtConfig::default();
let mut bootstrap = self.dht_saved_nodes.clone();
bootstrap.extend(default.bootstrap_nodes.iter().cloned());
irontide_dht::DhtConfig {
bootstrap_nodes: bootstrap,
own_id: self.dht_node_id,
queries_per_second: self.dht_queries_per_second,
query_timeout: std::time::Duration::from_secs(self.dht_query_timeout_secs),
enforce_node_id: self.dht_enforce_node_id,
restrict_routing_ips: self.dht_restrict_routing_ips,
dht_max_items: self.dht_max_items,
dht_item_lifetime_secs: self.dht_item_lifetime_secs,
state_dir: self.resume_data_dir.clone(),
read_only_mode: self.dht_read_only,
..default
}
}
pub(crate) fn to_dht_config_v6(&self) -> irontide_dht::DhtConfig {
let default = irontide_dht::DhtConfig::default_v6();
let mut bootstrap = self.dht_saved_nodes.clone();
bootstrap.extend(default.bootstrap_nodes.iter().cloned());
irontide_dht::DhtConfig {
bootstrap_nodes: bootstrap,
queries_per_second: self.dht_queries_per_second,
query_timeout: std::time::Duration::from_secs(self.dht_query_timeout_secs),
enforce_node_id: self.dht_enforce_node_id,
restrict_routing_ips: self.dht_restrict_routing_ips,
dht_max_items: self.dht_max_items,
dht_item_lifetime_secs: self.dht_item_lifetime_secs,
state_dir: self.resume_data_dir.clone(),
read_only_mode: self.dht_read_only,
..default
}
}
pub(crate) fn to_nat_config(&self) -> irontide_nat::NatConfig {
irontide_nat::NatConfig {
enable_upnp: self.enable_upnp,
enable_natpmp: self.enable_natpmp,
upnp_lease_duration: self.upnp_lease_duration,
natpmp_lifetime: self.natpmp_lifetime,
}
}
pub(crate) fn to_utp_config(&self, port: u16) -> irontide_utp::UtpConfig {
irontide_utp::UtpConfig {
bind_addr: std::net::SocketAddr::from(([0, 0, 0, 0], port)),
max_connections: self.utp_max_connections,
dscp: self.peer_dscp,
}
}
pub(crate) fn to_utp_config_v6(&self, port: u16) -> irontide_utp::UtpConfig {
irontide_utp::UtpConfig {
bind_addr: std::net::SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, port)),
max_connections: self.utp_max_connections,
dscp: self.peer_dscp,
}
}
pub(crate) fn to_sam_tunnel_config(&self) -> crate::i2p::SamTunnelConfig {
crate::i2p::SamTunnelConfig {
inbound_quantity: self.i2p_inbound_quantity,
outbound_quantity: self.i2p_outbound_quantity,
inbound_length: self.i2p_inbound_length,
outbound_length: self.i2p_outbound_length,
}
}
}
impl PartialEq for Settings {
fn eq(&self, other: &Self) -> bool {
self.listen_port == other.listen_port
&& self.download_dir == other.download_dir
&& self.max_torrents == other.max_torrents
&& self.resume_data_dir == other.resume_data_dir
&& self.save_resume_interval_secs == other.save_resume_interval_secs
&& self.enable_dht == other.enable_dht
&& self.enable_pex == other.enable_pex
&& self.enable_lsd == other.enable_lsd
&& self.enable_fast_extension == other.enable_fast_extension
&& self.enable_utp == other.enable_utp
&& self.enable_upnp == other.enable_upnp
&& self.enable_natpmp == other.enable_natpmp
&& self.enable_ipv6 == other.enable_ipv6
&& self.enable_web_seed == other.enable_web_seed
&& self.enable_holepunch == other.enable_holepunch
&& self.enable_bep40_eviction == other.enable_bep40_eviction
&& self.encryption_mode == other.encryption_mode
&& self.anonymous_mode == other.anonymous_mode
&& self.external_ip == other.external_ip
&& self.seed_ratio_limit == other.seed_ratio_limit
&& self.default_super_seeding == other.default_super_seeding
&& self.default_share_mode == other.default_share_mode
&& self.upload_only_announce == other.upload_only_announce
&& self.upload_rate_limit == other.upload_rate_limit
&& self.download_rate_limit == other.download_rate_limit
&& self.tcp_upload_rate_limit == other.tcp_upload_rate_limit
&& self.tcp_download_rate_limit == other.tcp_download_rate_limit
&& self.utp_upload_rate_limit == other.utp_upload_rate_limit
&& self.utp_download_rate_limit == other.utp_download_rate_limit
&& self.auto_upload_slots == other.auto_upload_slots
&& self.auto_upload_slots_min == other.auto_upload_slots_min
&& self.auto_upload_slots_max == other.auto_upload_slots_max
&& self.mixed_mode_algorithm == other.mixed_mode_algorithm
&& self.active_downloads == other.active_downloads
&& self.active_seeds == other.active_seeds
&& self.active_limit == other.active_limit
&& self.active_checking == other.active_checking
&& self.dont_count_slow_torrents == other.dont_count_slow_torrents
&& self.inactive_down_rate == other.inactive_down_rate
&& self.inactive_up_rate == other.inactive_up_rate
&& self.auto_manage_interval == other.auto_manage_interval
&& self.auto_manage_startup == other.auto_manage_startup
&& self.auto_manage_prefer_seeds == other.auto_manage_prefer_seeds
&& self.alert_mask == other.alert_mask
&& self.alert_channel_size == other.alert_channel_size
&& self.smart_ban_max_failures == other.smart_ban_max_failures
&& self.smart_ban_parole == other.smart_ban_parole
&& self.disk_io_threads == other.disk_io_threads
&& self.max_blocking_threads == other.max_blocking_threads
&& self.storage_mode == other.storage_mode
&& self.disk_cache_size == other.disk_cache_size
&& self.disk_write_cache_ratio.to_bits() == other.disk_write_cache_ratio.to_bits()
&& self.disk_channel_capacity == other.disk_channel_capacity
&& self.buffer_pool_capacity == other.buffer_pool_capacity
&& self.enable_mlock == other.enable_mlock
&& self.hashing_threads == other.hashing_threads
&& self.max_request_queue_depth == other.max_request_queue_depth
&& self.initial_queue_depth == other.initial_queue_depth
&& self.request_queue_time.to_bits() == other.request_queue_time.to_bits()
&& self.block_request_timeout_secs == other.block_request_timeout_secs
&& self.max_concurrent_stream_reads == other.max_concurrent_stream_reads
&& self.auto_sequential == other.auto_sequential
&& self.steal_threshold_ratio.to_bits() == other.steal_threshold_ratio.to_bits()
&& self.use_block_stealing == other.use_block_stealing
&& self.steal_stale_piece_secs == other.steal_stale_piece_secs
&& self.steal_threshold_endgame.to_bits() == other.steal_threshold_endgame.to_bits()
&& self.min_pipeline_depth == other.min_pipeline_depth
&& self.max_pipeline_depth == other.max_pipeline_depth
&& self.target_buffer_secs.to_bits() == other.target_buffer_secs.to_bits()
&& self.fixed_pipeline_depth == other.fixed_pipeline_depth
&& self.strict_end_game == other.strict_end_game
&& self.max_web_seeds == other.max_web_seeds
&& self.initial_picker_threshold == other.initial_picker_threshold
&& self.whole_pieces_threshold == other.whole_pieces_threshold
&& self.snub_timeout_secs == other.snub_timeout_secs
&& self.readahead_pieces == other.readahead_pieces
&& self.streaming_timeout_escalation == other.streaming_timeout_escalation
&& self.piece_extent_affinity == other.piece_extent_affinity
&& self.suggest_mode == other.suggest_mode
&& self.max_suggest_pieces == other.max_suggest_pieces
&& self.predictive_piece_announce_ms == other.predictive_piece_announce_ms
&& self.force_proxy == other.force_proxy
&& self.apply_ip_filter_to_trackers == other.apply_ip_filter_to_trackers
&& self.dht_queries_per_second == other.dht_queries_per_second
&& self.dht_query_timeout_secs == other.dht_query_timeout_secs
&& self.dht_enforce_node_id == other.dht_enforce_node_id
&& self.dht_restrict_routing_ips == other.dht_restrict_routing_ips
&& self.dht_max_items == other.dht_max_items
&& self.dht_item_lifetime_secs == other.dht_item_lifetime_secs
&& self.dht_sample_infohashes_interval == other.dht_sample_infohashes_interval
&& self.dht_read_only == other.dht_read_only
&& self.upnp_lease_duration == other.upnp_lease_duration
&& self.natpmp_lifetime == other.natpmp_lifetime
&& self.utp_max_connections == other.utp_max_connections
&& self.enable_i2p == other.enable_i2p
&& self.i2p_hostname == other.i2p_hostname
&& self.i2p_port == other.i2p_port
&& self.i2p_inbound_quantity == other.i2p_inbound_quantity
&& self.i2p_outbound_quantity == other.i2p_outbound_quantity
&& self.i2p_inbound_length == other.i2p_inbound_length
&& self.i2p_outbound_length == other.i2p_outbound_length
&& self.allow_i2p_mixed == other.allow_i2p_mixed
&& self.ssl_listen_port == other.ssl_listen_port
&& self.ssl_cert_path == other.ssl_cert_path
&& self.ssl_key_path == other.ssl_key_path
&& self.seed_choking_algorithm == other.seed_choking_algorithm
&& self.choking_algorithm == other.choking_algorithm
&& self.max_peers_per_torrent == other.max_peers_per_torrent
&& self.peer_read_timeout_secs == other.peer_read_timeout_secs
&& self.peer_write_timeout_secs == other.peer_write_timeout_secs
&& self.data_contribution_timeout_secs == other.data_contribution_timeout_secs
&& self.choke_rotation_max_evictions == other.choke_rotation_max_evictions
&& self.max_concurrent_connects == other.max_concurrent_connects
&& self.connect_soft_timeout == other.connect_soft_timeout
&& self.ssrf_mitigation == other.ssrf_mitigation
&& self.allow_idna == other.allow_idna
&& self.validate_https_trackers == other.validate_https_trackers
&& self.max_metadata_size == other.max_metadata_size
&& self.max_message_size == other.max_message_size
&& self.max_piece_length == other.max_piece_length
&& self.max_outstanding_requests == other.max_outstanding_requests
&& self.max_in_flight_pieces == other.max_in_flight_pieces
&& self.peer_connect_timeout == other.peer_connect_timeout
&& self.peer_dscp == other.peer_dscp
&& self.stats_report_interval == other.stats_report_interval
&& self.runtime_worker_threads == other.runtime_worker_threads
&& self.pin_cores == other.pin_cores
&& self.dht_saved_nodes == other.dht_saved_nodes
&& self.dht_node_id == other.dht_node_id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_settings_values() {
let s = Settings::default();
assert_eq!(s.listen_port, 42020);
assert_eq!(s.download_dir, PathBuf::from("."));
assert_eq!(s.max_torrents, 100);
assert!(s.resume_data_dir.is_none());
assert_eq!(s.save_resume_interval_secs, 300);
assert!(s.enable_dht);
assert!(s.enable_pex);
assert!(s.enable_lsd);
assert!(s.enable_fast_extension);
assert!(s.enable_utp);
assert!(s.enable_upnp);
assert!(s.enable_natpmp);
assert!(s.enable_ipv6);
assert!(s.enable_web_seed);
assert_eq!(s.encryption_mode, EncryptionMode::Disabled);
assert!(!s.anonymous_mode);
assert!(s.seed_ratio_limit.is_none());
assert!(!s.default_super_seeding);
assert!(!s.default_share_mode);
assert!(s.upload_only_announce);
assert_eq!(s.upload_rate_limit, 0);
assert_eq!(s.download_rate_limit, 0);
assert!(s.auto_upload_slots);
assert_eq!(s.active_downloads, 3);
assert_eq!(s.active_seeds, 5);
assert_eq!(s.active_limit, 500);
assert_eq!(s.active_checking, 1);
assert!(s.dont_count_slow_torrents);
assert_eq!(s.alert_mask, AlertCategory::ALL);
assert_eq!(s.alert_channel_size, 1024);
assert_eq!(s.smart_ban_max_failures, 3);
assert!(s.smart_ban_parole);
assert_eq!(s.disk_io_threads, default_disk_io_threads());
assert_eq!(s.max_blocking_threads, default_max_blocking_threads());
assert_eq!(s.storage_mode, StorageMode::Auto);
assert_eq!(s.disk_cache_size, 16 * 1024 * 1024);
assert!((s.disk_write_cache_ratio - 0.5).abs() < f32::EPSILON);
assert_eq!(s.disk_channel_capacity, 512);
assert_eq!(s.hashing_threads, default_hashing_threads());
assert_eq!(s.max_request_queue_depth, 250);
assert_eq!(s.initial_queue_depth, 128);
assert!((s.request_queue_time - 3.0).abs() < f64::EPSILON);
assert_eq!(s.block_request_timeout_secs, 60);
assert_eq!(s.max_concurrent_stream_reads, 8);
assert!(!s.force_proxy);
assert!(s.apply_ip_filter_to_trackers);
assert_eq!(s.dht_queries_per_second, 50);
assert_eq!(s.dht_query_timeout_secs, 5);
assert!(!s.dht_enforce_node_id);
assert!(s.dht_restrict_routing_ips);
assert_eq!(s.upnp_lease_duration, 3600);
assert_eq!(s.natpmp_lifetime, 7200);
assert_eq!(s.utp_max_connections, 256);
assert_eq!(s.mixed_mode_algorithm, MixedModeAlgorithm::PeerProportional);
assert!(s.auto_sequential);
assert!(s.strict_end_game);
assert_eq!(s.max_web_seeds, 4);
assert_eq!(s.initial_picker_threshold, 4);
assert_eq!(s.whole_pieces_threshold, 20);
assert_eq!(s.snub_timeout_secs, 15);
assert_eq!(s.readahead_pieces, 8);
assert!(s.streaming_timeout_escalation);
assert_eq!(s.max_peers_per_torrent, 128);
assert_eq!(s.runtime_worker_threads, default_runtime_worker_threads());
assert!(s.pin_cores);
}
#[test]
fn min_memory_preset() {
let s = Settings::min_memory();
assert_eq!(s.disk_cache_size, 8 * 1024 * 1024);
assert_eq!(s.max_torrents, 20);
assert_eq!(s.max_peers_per_torrent, 30);
assert_eq!(s.active_downloads, 1);
assert_eq!(s.active_seeds, 2);
assert_eq!(s.active_limit, 10);
assert_eq!(s.alert_channel_size, 256);
assert_eq!(s.utp_max_connections, 64);
assert_eq!(s.max_request_queue_depth, 50);
assert_eq!(s.initial_queue_depth, 16);
assert_eq!(s.max_concurrent_stream_reads, 2);
assert_eq!(s.hashing_threads, 1);
assert_eq!(s.disk_io_threads, 1);
}
#[test]
fn high_performance_preset() {
let s = Settings::high_performance();
assert_eq!(s.disk_cache_size, 256 * 1024 * 1024);
assert_eq!(s.max_torrents, 2000);
assert_eq!(s.max_peers_per_torrent, 200);
assert_eq!(s.active_downloads, 30);
assert_eq!(s.active_seeds, 100);
assert_eq!(s.active_limit, 2000);
assert_eq!(s.alert_channel_size, 4096);
assert_eq!(s.utp_max_connections, 1024);
assert_eq!(s.max_request_queue_depth, 1000);
assert_eq!(s.initial_queue_depth, 256);
assert_eq!(s.max_concurrent_stream_reads, 32);
assert_eq!(s.hashing_threads, 4);
assert_eq!(s.disk_io_threads, 8);
assert_eq!(s.auto_upload_slots_max, 100);
}
#[test]
fn json_round_trip() {
let original = Settings::default();
let json = serde_json::to_string(&original).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn json_round_trip_presets() {
for original in [Settings::min_memory(), Settings::high_performance()] {
let json = serde_json::to_string(&original).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(original, decoded);
}
}
#[test]
fn json_missing_fields_use_defaults() {
let decoded: Settings = serde_json::from_str("{}").unwrap();
assert_eq!(decoded, Settings::default());
}
#[test]
fn validation_force_proxy_no_proxy() {
let mut s = Settings::default();
s.force_proxy = true;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("force_proxy"));
}
#[test]
fn validation_valid_defaults() {
Settings::default().validate().unwrap();
Settings::min_memory().validate().unwrap();
Settings::high_performance().validate().unwrap();
}
#[test]
fn disk_config_from_settings() {
let s = Settings::default();
let dc = crate::disk::DiskConfig::from(&s);
assert_eq!(dc.io_threads, default_disk_io_threads());
assert_eq!(dc.storage_mode, StorageMode::Auto);
assert_eq!(dc.cache_size, 16 * 1024 * 1024);
assert!((dc.write_cache_ratio - 0.5).abs() < f32::EPSILON);
assert_eq!(dc.channel_capacity, 512);
}
#[test]
fn torrent_config_from_settings() {
let s = Settings::default();
let tc = crate::types::TorrentConfig::from(&s);
assert_eq!(tc.listen_port, 0); assert_eq!(tc.max_peers, s.max_peers_per_torrent);
assert_eq!(tc.download_dir, s.download_dir);
assert_eq!(tc.enable_dht, s.enable_dht);
assert_eq!(tc.enable_pex, s.enable_pex);
assert_eq!(tc.encryption_mode, s.encryption_mode);
assert_eq!(tc.enable_utp, s.enable_utp);
assert_eq!(tc.enable_web_seed, s.enable_web_seed);
assert_eq!(tc.hashing_threads, s.hashing_threads);
assert_eq!(
tc.max_concurrent_stream_reads,
s.max_concurrent_stream_reads
);
assert_eq!(tc.anonymous_mode, s.anonymous_mode);
assert_eq!(tc.enable_i2p, s.enable_i2p);
assert_eq!(tc.allow_i2p_mixed, s.allow_i2p_mixed);
assert_eq!(tc.strict_end_game, s.strict_end_game);
assert_eq!(tc.upload_rate_limit, s.upload_rate_limit);
assert_eq!(tc.download_rate_limit, s.download_rate_limit);
assert_eq!(tc.max_web_seeds, s.max_web_seeds);
assert_eq!(tc.initial_picker_threshold, s.initial_picker_threshold);
assert_eq!(tc.whole_pieces_threshold, s.whole_pieces_threshold);
assert_eq!(tc.snub_timeout_secs, s.snub_timeout_secs);
assert_eq!(tc.readahead_pieces, s.readahead_pieces);
assert_eq!(
tc.streaming_timeout_escalation,
s.streaming_timeout_escalation
);
assert_eq!(tc.storage_mode, s.storage_mode);
assert_eq!(tc.block_request_timeout_secs, s.block_request_timeout_secs);
assert_eq!(tc.enable_lsd, s.enable_lsd);
assert_eq!(tc.force_proxy, s.force_proxy);
assert_eq!(tc.steal_stale_piece_secs, 2);
assert_eq!(tc.steal_stale_piece_secs, s.steal_stale_piece_secs);
}
#[test]
fn torrent_config_from_nondefault_settings() {
let mut s = Settings::default();
s.strict_end_game = false;
s.upload_rate_limit = 1_000_000;
s.download_rate_limit = 2_000_000;
s.max_web_seeds = 8;
s.initial_picker_threshold = 10;
s.whole_pieces_threshold = 50;
s.snub_timeout_secs = 120;
s.readahead_pieces = 16;
s.streaming_timeout_escalation = false;
s.storage_mode = StorageMode::Full;
s.block_request_timeout_secs = 30;
s.enable_lsd = false;
s.force_proxy = true;
s.proxy.proxy_type = crate::proxy::ProxyType::Socks5;
let tc = crate::types::TorrentConfig::from(&s);
assert!(!tc.strict_end_game);
assert_eq!(tc.upload_rate_limit, 1_000_000);
assert_eq!(tc.download_rate_limit, 2_000_000);
assert_eq!(tc.max_web_seeds, 8);
assert_eq!(tc.initial_picker_threshold, 10);
assert_eq!(tc.whole_pieces_threshold, 50);
assert_eq!(tc.snub_timeout_secs, 120);
assert_eq!(tc.readahead_pieces, 16);
assert!(!tc.streaming_timeout_escalation);
assert_eq!(tc.storage_mode, StorageMode::Full);
assert_eq!(tc.block_request_timeout_secs, 30);
assert!(!tc.enable_lsd);
assert!(tc.force_proxy);
}
#[test]
fn external_ip_default_and_json() {
let s = Settings::default();
assert!(s.external_ip.is_none());
let json = r#"{"external_ip": "203.0.113.5"}"#;
let decoded: Settings = serde_json::from_str(json).unwrap();
assert_eq!(
decoded.external_ip,
Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(
203, 0, 113, 5
)))
);
let encoded = serde_json::to_string(&decoded).unwrap();
let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
assert_eq!(roundtrip.external_ip, decoded.external_ip);
}
#[test]
fn validation_zero_threads() {
let mut s = Settings::default();
s.hashing_threads = 0;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("hashing_threads"));
let mut s = Settings::default();
s.disk_io_threads = 0;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("disk_io_threads"));
let mut s = Settings::default();
s.max_blocking_threads = 0;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("max_blocking_threads"));
}
#[test]
fn share_mode_requires_fast_extension() {
let mut s = Settings::default();
s.default_share_mode = true;
s.enable_fast_extension = false;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("share_mode"));
s.enable_fast_extension = true;
s.validate().unwrap();
}
#[test]
fn share_mode_default_false() {
let cfg = crate::types::TorrentConfig::default();
assert!(!cfg.share_mode);
}
#[test]
fn dht_storage_settings_defaults() {
let s = Settings::default();
assert_eq!(s.dht_max_items, 700);
assert_eq!(s.dht_item_lifetime_secs, 7200);
}
#[test]
fn dht_sample_interval_default_disabled() {
let s = Settings::default();
assert_eq!(s.dht_sample_infohashes_interval, 0);
}
#[test]
fn dht_sample_interval_json_round_trip() {
let json = r#"{"dht_sample_infohashes_interval": 300}"#;
let decoded: Settings = serde_json::from_str(json).unwrap();
assert_eq!(decoded.dht_sample_infohashes_interval, 300);
let encoded = serde_json::to_string(&decoded).unwrap();
let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
assert_eq!(roundtrip.dht_sample_infohashes_interval, 300);
}
#[test]
fn min_memory_restricts_dht_items() {
let s = Settings::min_memory();
assert_eq!(s.dht_max_items, 100);
}
#[test]
fn dht_config_inherits_security_settings() {
let mut s = Settings::default();
s.dht_enforce_node_id = false;
let dht = s.to_dht_config();
assert!(!dht.enforce_node_id);
assert!(dht.restrict_routing_ips);
let dht_v6 = s.to_dht_config_v6();
assert!(!dht_v6.enforce_node_id);
assert!(dht_v6.restrict_routing_ips);
}
#[test]
fn enable_holepunch_default_true() {
let s = Settings::default();
assert!(s.enable_holepunch);
}
#[test]
fn enable_holepunch_json_round_trip() {
let json = r#"{"enable_holepunch": false}"#;
let decoded: Settings = serde_json::from_str(json).unwrap();
assert!(!decoded.enable_holepunch);
let encoded = serde_json::to_string(&decoded).unwrap();
let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
assert!(!roundtrip.enable_holepunch);
}
#[test]
fn i2p_settings_defaults() {
let s = Settings::default();
assert!(!s.enable_i2p);
assert_eq!(s.i2p_hostname, "127.0.0.1");
assert_eq!(s.i2p_port, 7656);
assert_eq!(s.i2p_inbound_quantity, 3);
assert_eq!(s.i2p_outbound_quantity, 3);
assert_eq!(s.i2p_inbound_length, 3);
assert_eq!(s.i2p_outbound_length, 3);
assert!(!s.allow_i2p_mixed);
}
#[test]
fn i2p_settings_json_roundtrip() {
let mut s = Settings::default();
s.enable_i2p = true;
s.i2p_hostname = "10.0.0.1".into();
s.i2p_port = 7700;
s.i2p_inbound_quantity = 5;
s.i2p_outbound_quantity = 4;
s.i2p_inbound_length = 2;
s.i2p_outbound_length = 1;
s.allow_i2p_mixed = true;
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(s, decoded);
}
#[test]
fn i2p_validation_quantity_zero() {
let mut s = Settings::default();
s.enable_i2p = true;
s.i2p_inbound_quantity = 0;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("i2p_inbound_quantity"));
}
#[test]
fn i2p_validation_quantity_too_high() {
let mut s = Settings::default();
s.enable_i2p = true;
s.i2p_outbound_quantity = 17;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("i2p_outbound_quantity"));
}
#[test]
fn i2p_validation_length_too_high() {
let mut s = Settings::default();
s.enable_i2p = true;
s.i2p_inbound_length = 8;
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("i2p_inbound_length"));
}
#[test]
fn i2p_validation_passes_when_disabled() {
let mut s = Settings::default();
s.enable_i2p = false;
s.i2p_inbound_quantity = 0; s.validate().unwrap(); }
#[test]
fn i2p_validation_valid_config() {
let mut s = Settings::default();
s.enable_i2p = true;
s.i2p_inbound_quantity = 1;
s.i2p_outbound_quantity = 16;
s.i2p_inbound_length = 0;
s.i2p_outbound_length = 7;
s.validate().unwrap();
}
#[test]
fn ssl_settings_defaults() {
let s = Settings::default();
assert_eq!(s.ssl_listen_port, 0);
assert!(s.ssl_cert_path.is_none());
assert!(s.ssl_key_path.is_none());
}
#[test]
fn ssl_settings_json_round_trip() {
let mut s = Settings::default();
s.ssl_listen_port = 4433;
s.ssl_cert_path = Some(PathBuf::from("/etc/ssl/cert.pem"));
s.ssl_key_path = Some(PathBuf::from("/etc/ssl/key.pem"));
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(s, decoded);
}
#[test]
fn ssl_validation_cert_without_key() {
let mut s = Settings::default();
s.ssl_cert_path = Some(PathBuf::from("/tmp/cert.pem"));
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("ssl_cert_path"));
}
#[test]
fn ssl_validation_key_without_cert() {
let mut s = Settings::default();
s.ssl_key_path = Some(PathBuf::from("/tmp/key.pem"));
let err = s.validate().unwrap_err();
assert!(err.to_string().contains("ssl_cert_path"));
}
#[test]
fn ssl_validation_both_set_passes() {
let mut s = Settings::default();
s.ssl_cert_path = Some(PathBuf::from("/tmp/cert.pem"));
s.ssl_key_path = Some(PathBuf::from("/tmp/key.pem"));
s.validate().unwrap();
}
#[test]
fn ssl_validation_both_absent_passes() {
let s = Settings::default();
s.validate().unwrap();
}
#[test]
fn default_choking_algorithms() {
let s = Settings::default();
assert_eq!(
s.seed_choking_algorithm,
SeedChokingAlgorithm::FastestUpload
);
assert_eq!(s.choking_algorithm, ChokingAlgorithm::FixedSlots);
}
#[test]
fn choking_algorithm_json_round_trip() {
let mut s = Settings::default();
s.seed_choking_algorithm = SeedChokingAlgorithm::AntiLeech;
s.choking_algorithm = ChokingAlgorithm::RateBased;
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(
decoded.seed_choking_algorithm,
SeedChokingAlgorithm::AntiLeech
);
assert_eq!(decoded.choking_algorithm, ChokingAlgorithm::RateBased);
}
#[test]
fn m44_settings_defaults() {
let s = Settings::default();
assert!(s.piece_extent_affinity);
assert!(!s.suggest_mode);
assert_eq!(s.max_suggest_pieces, 16);
assert_eq!(s.predictive_piece_announce_ms, 0);
}
#[test]
fn m44_high_performance_enables_suggest() {
let s = Settings::high_performance();
assert!(s.suggest_mode);
}
#[test]
fn m44_json_round_trip() {
let mut s = Settings::default();
s.piece_extent_affinity = false;
s.suggest_mode = true;
s.max_suggest_pieces = 5;
s.predictive_piece_announce_ms = 50;
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(s, decoded);
}
#[test]
fn security_settings_defaults() {
let s = Settings::default();
assert!(s.ssrf_mitigation);
assert!(!s.allow_idna);
assert!(s.validate_https_trackers);
}
#[test]
fn security_settings_json_round_trip() {
let mut s = Settings::default();
s.ssrf_mitigation = false;
s.allow_idna = true;
s.validate_https_trackers = false;
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(s, decoded);
}
#[test]
fn security_settings_missing_use_defaults() {
let decoded: Settings = serde_json::from_str("{}").unwrap();
assert!(decoded.ssrf_mitigation);
assert!(!decoded.allow_idna);
assert!(decoded.validate_https_trackers);
}
#[test]
fn url_security_config_from_settings() {
let mut s = Settings::default();
s.ssrf_mitigation = false;
s.allow_idna = true;
s.validate_https_trackers = false;
let cfg = crate::url_guard::UrlSecurityConfig::from(&s);
assert!(!cfg.ssrf_mitigation);
assert!(cfg.allow_idna);
assert!(!cfg.validate_https_trackers);
}
#[test]
fn default_peer_dscp_value() {
let s = Settings::default();
assert_eq!(s.peer_dscp, 0x08);
}
#[test]
fn peer_dscp_json_round_trip() {
let mut s = Settings::default();
s.peer_dscp = 0x2E; let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.peer_dscp, 0x2E);
}
#[test]
fn peer_dscp_zero_disables() {
let mut s = Settings::default();
s.peer_dscp = 0;
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.peer_dscp, 0);
}
#[test]
fn utp_config_includes_dscp() {
let mut s = Settings::default();
s.peer_dscp = 0x0A;
let utp = s.to_utp_config(6881);
assert_eq!(utp.dscp, 0x0A);
let utp_v6 = s.to_utp_config_v6(6881);
assert_eq!(utp_v6.dscp, 0x0A);
}
#[test]
fn default_stats_report_interval() {
let s = Settings::default();
assert_eq!(s.stats_report_interval, 1000);
}
#[test]
fn stats_report_interval_json_round_trip() {
let mut s = Settings::default();
s.stats_report_interval = 5000;
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.stats_report_interval, 5000);
}
#[test]
fn stats_report_interval_zero_disables() {
let mut s = Settings::default();
s.stats_report_interval = 0;
let json = serde_json::to_string(&s).unwrap();
let decoded: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.stats_report_interval, 0);
}
#[test]
fn settings_runtime_worker_threads_and_pin_cores() {
let s = Settings::default();
assert_eq!(s.runtime_worker_threads, default_runtime_worker_threads());
assert!(s.pin_cores);
let mut s = Settings::default();
s.runtime_worker_threads = 0;
assert!(s.validate().is_ok());
s.runtime_worker_threads = 256;
assert!(s.validate().is_ok());
s.runtime_worker_threads = 257;
assert!(s.validate().is_err());
}
#[test]
fn max_in_flight_512_default() {
let s = Settings::default();
assert_eq!(s.max_in_flight_pieces, 512);
assert_eq!(s.fixed_pipeline_depth, 128);
let mm = Settings::min_memory();
assert_eq!(mm.max_in_flight_pieces, 32);
assert_eq!(mm.fixed_pipeline_depth, 32);
let hp = Settings::high_performance();
assert_eq!(hp.max_in_flight_pieces, 512);
assert_eq!(hp.fixed_pipeline_depth, 128); }
#[test]
fn recalc_max_in_flight_formula() {
let base = 512_usize;
let connected = 10;
let num_pieces = 2000_u32;
let calculated = base.max(connected * 4);
let result = calculated.min(num_pieces as usize / 2).max(base);
assert_eq!(result, 512);
let connected = 200;
let calculated = base.max(connected * 4);
let result = calculated.min(num_pieces as usize / 2).max(base);
assert_eq!(result, 800);
let connected = 200;
let num_pieces = 100_u32;
let calculated = base.max(connected * 4);
let result = calculated.min(num_pieces as usize / 2).max(base);
assert_eq!(result, 512);
let connected = 129; let num_pieces = 10000_u32;
let calculated = base.max(connected * 4);
let result = calculated.min(num_pieces as usize / 2).max(base);
assert_eq!(result, 516); }
}