1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum MaxRatioAction {
30 #[default]
32 Pause,
33 Remove,
35 EnableSuperSeeding,
37}
38
39fn default_true() -> bool {
42 true
43}
44fn default_listen_port() -> u16 {
45 42020
46}
47fn default_download_dir() -> PathBuf {
48 PathBuf::from(".")
49}
50fn default_max_torrents() -> usize {
51 100
52}
53fn default_encryption() -> EncryptionMode {
54 EncryptionMode::Disabled
55}
56fn default_auto_upload_slots_min() -> usize {
57 2
58}
59fn default_auto_upload_slots_max() -> usize {
60 20
61}
62fn default_active_downloads() -> i32 {
63 3
64}
65fn default_active_seeds() -> i32 {
66 5
67}
68fn default_active_limit() -> i32 {
69 500
70}
71fn default_active_checking() -> i32 {
72 3
73}
74fn default_inactive_rate() -> u64 {
75 2048
76}
77fn default_auto_manage_interval() -> u64 {
78 30
79}
80fn default_auto_manage_startup() -> u64 {
81 60
82}
83fn default_queue_rate_ewma_alpha() -> f64 {
84 0.3
85}
86fn default_seed_queue_min_active_secs() -> u64 {
87 1800
88}
89fn default_alert_mask() -> AlertCategory {
90 AlertCategory::ALL
91}
92fn default_alert_channel_size() -> usize {
93 1024
94}
95fn default_smart_ban_max_failures() -> u32 {
96 3
97}
98fn default_disk_io_threads() -> usize {
99 let cores = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
100 (cores / 2).clamp(4, 16)
101}
102fn default_max_blocking_threads() -> usize {
103 std::thread::available_parallelism().map_or(4, std::num::NonZero::get)
104}
105fn default_storage_mode() -> StorageMode {
106 StorageMode::Auto
107}
108fn default_disk_cache_size() -> usize {
109 16 * 1024 * 1024
110}
111fn default_disk_write_cache_ratio() -> f32 {
112 0.5
113}
114fn default_buffer_pool_capacity() -> usize {
115 64 * 1024 * 1024
116}
117fn default_enable_mlock() -> bool {
118 cfg!(unix)
119}
120fn default_io_uring_sq_depth() -> u32 {
121 256
122}
123fn default_io_uring_batch_threshold() -> usize {
124 4
125}
126fn default_disk_channel_capacity() -> usize {
127 512
128}
129fn default_hashing_threads() -> usize {
130 let cores = std::thread::available_parallelism().map_or(4, std::num::NonZero::get);
131 (cores / 4).clamp(2, 8)
132}
133fn default_max_request_queue_depth() -> usize {
134 250
135}
136fn default_initial_queue_depth() -> usize {
137 128
138}
139fn default_request_queue_time() -> f64 {
140 3.0
141}
142fn default_block_request_timeout() -> u32 {
143 60
144}
145fn default_max_concurrent_streams() -> usize {
146 8
147}
148fn default_dht_qps() -> usize {
149 50
150}
151fn default_dht_timeout() -> u64 {
152 5
153}
154fn default_upnp_lease() -> u32 {
155 3600
156}
157fn default_natpmp_lifetime() -> u32 {
158 7200
159}
160fn default_utp_max_conns() -> usize {
161 256
162}
163fn default_dht_max_items() -> usize {
164 700
165}
166fn default_dht_item_lifetime() -> u64 {
167 7200
168}
169fn default_dht_sample_interval() -> u64 {
170 0
171}
172fn default_suggest_mode() -> bool {
173 true
174}
175fn default_max_suggest_pieces() -> usize {
176 16
177}
178fn default_predictive_piece_announce_ms() -> u64 {
179 0
180}
181fn default_ssl_listen_port() -> u16 {
182 0 }
184fn default_seed_choking_algorithm() -> SeedChokingAlgorithm {
185 SeedChokingAlgorithm::FastestUpload
186}
187fn default_choking_algorithm() -> ChokingAlgorithm {
188 ChokingAlgorithm::FixedSlots
189}
190fn default_mixed_mode() -> MixedModeAlgorithm {
191 MixedModeAlgorithm::PeerProportional
192}
193fn default_steal_threshold_ratio() -> f64 {
194 10.0
195}
196fn default_use_block_stealing() -> bool {
197 true
198}
199fn default_peer_connect_timeout() -> u64 {
200 10 }
202fn default_peer_dscp() -> u8 {
203 0x08 }
205fn default_max_peers_per_torrent() -> usize {
206 128
207}
208fn default_pass0_grace_secs() -> u64 {
210 60 }
212fn default_proactive_evictions_per_minute_limit() -> u32 {
213 30 }
215fn default_eviction_ban_duration_secs() -> u64 {
216 600 }
219fn default_eviction_ban_set_cap() -> usize {
220 1024 }
222fn default_stats_report_interval() -> u64 {
223 1000
224}
225fn default_strict_end_game() -> bool {
226 true
227}
228fn default_max_web_seeds() -> usize {
229 4
230}
231fn default_web_seed_retry_base_secs() -> u64 {
232 10
233}
234fn default_web_seed_retry_factor() -> u64 {
235 6
236}
237fn default_web_seed_retry_cap_secs() -> u64 {
238 3600
239}
240fn default_web_seed_max_failures() -> u32 {
241 10
242}
243fn default_initial_picker_threshold() -> u32 {
244 4
245}
246fn default_whole_pieces_threshold() -> u32 {
247 20
248}
249fn default_snub_timeout_secs() -> u32 {
250 15
251}
252fn default_readahead_pieces() -> u32 {
253 8
254}
255fn default_max_metadata_size() -> u64 {
256 4 * 1024 * 1024 }
258fn default_max_message_size() -> usize {
259 16 * 1024 * 1024 }
261fn default_max_piece_length() -> u64 {
262 32 * 1024 * 1024 }
264fn default_max_outstanding_requests() -> usize {
265 500
266}
267fn default_max_in_flight_pieces() -> usize {
268 512
269}
270fn default_fixed_pipeline_depth() -> usize {
271 128
272}
273fn default_i2p_hostname() -> String {
274 "127.0.0.1".into()
275}
276fn default_i2p_port() -> u16 {
277 7656
278}
279fn default_i2p_tunnel_quantity() -> u8 {
280 3
281}
282fn default_i2p_tunnel_length() -> u8 {
283 3
284}
285fn default_runtime_worker_threads() -> usize {
286 std::thread::available_parallelism().map_or(4, |n| n.get().min(8))
287}
288fn default_lock_warn_threshold_ms() -> u64 {
289 50
290}
291fn default_steal_stale_piece_secs() -> u64 {
292 2
293}
294fn default_steal_threshold_endgame() -> f64 {
295 3.0
296}
297fn default_peer_read_timeout_secs() -> u64 {
298 10
299}
300fn default_peer_write_timeout_secs() -> u64 {
301 10
302}
303fn default_data_contribution_timeout() -> u64 {
304 0 }
306fn default_choke_rotation_max_evictions() -> u32 {
307 0 }
309fn default_max_concurrent_connects() -> u16 {
310 128 }
312fn default_connect_soft_timeout() -> u64 {
313 3 }
315fn default_dispatch_backlog_cap() -> usize {
316 8 }
318fn default_event_backlog_cap() -> usize {
319 32 }
321fn default_web_seed_progress_throttle_ms() -> u64 {
322 250 }
324fn default_save_resume_interval() -> u64 {
325 300 }
327fn default_max_upload_slots_global() -> i32 {
328 -1
329}
330fn default_max_upload_slots_per_torrent() -> i32 {
331 4
332}
333fn default_max_connections_global() -> i32 {
334 -1
335}
336fn default_max_uploads_per_torrent() -> i32 {
337 -1
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
348pub struct Settings {
349 #[serde(default = "default_listen_port")]
352 pub listen_port: u16,
353 #[serde(default)]
355 pub randomize_port_on_startup: bool,
356 #[serde(default = "default_download_dir")]
358 pub download_dir: PathBuf,
359 #[serde(default = "default_max_torrents")]
361 pub max_torrents: usize,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub resume_data_dir: Option<PathBuf>,
365 #[serde(default = "default_save_resume_interval")]
368 pub save_resume_interval_secs: u64,
369
370 #[serde(default = "default_true")]
373 pub enable_dht: bool,
374 #[serde(default = "default_true")]
376 pub enable_pex: bool,
377 #[serde(default = "default_true")]
379 pub enable_lsd: bool,
380 #[serde(default = "default_true")]
383 pub enable_fast_extension: bool,
384 #[serde(default = "default_true")]
388 pub enable_utp: bool,
389 #[serde(default = "default_true")]
392 pub enable_upnp: bool,
393 #[serde(default = "default_true")]
396 pub enable_natpmp: bool,
397 #[serde(default = "default_true")]
401 pub enable_ipv6: bool,
402 #[serde(default = "default_true")]
406 pub enable_web_seed: bool,
407 #[serde(default = "default_true")]
411 pub enable_holepunch: bool,
412 #[serde(default = "default_true")]
416 pub enable_bep40_eviction: bool,
417 #[serde(default)]
421 pub enable_diagnostic_counters: bool,
422 #[serde(default = "default_encryption")]
424 pub encryption_mode: EncryptionMode,
425 #[serde(default)]
428 pub anonymous_mode: bool,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
432 pub external_ip: Option<IpAddr>,
433
434 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub seed_ratio_limit: Option<f64>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub seed_time_limit_secs: Option<u64>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub inactive_seed_time_limit_secs: Option<u64>,
449 #[serde(default)]
452 pub max_ratio_action: MaxRatioAction,
453 #[serde(default = "default_true")]
457 pub create_subfolder: bool,
458 #[serde(default)]
462 pub auto_manage_torrents: bool,
463 #[serde(default)]
467 pub queueing_enabled: bool,
468 #[serde(default)]
471 pub default_super_seeding: bool,
472 #[serde(default)]
475 pub default_share_mode: bool,
476 #[serde(default = "default_true")]
479 pub upload_only_announce: bool,
480 #[serde(default)]
483 pub upload_rate_limit: u64,
484 #[serde(default)]
486 pub download_rate_limit: u64,
487 #[serde(default)]
489 pub tcp_upload_rate_limit: u64,
490 #[serde(default)]
492 pub tcp_download_rate_limit: u64,
493 #[serde(default)]
495 pub utp_upload_rate_limit: u64,
496 #[serde(default)]
498 pub utp_download_rate_limit: u64,
499 #[serde(default = "default_true")]
501 pub auto_upload_slots: bool,
502 #[serde(default = "default_auto_upload_slots_min")]
504 pub auto_upload_slots_min: usize,
505 #[serde(default = "default_auto_upload_slots_max")]
507 pub auto_upload_slots_max: usize,
508 #[serde(default = "default_max_upload_slots_global")]
510 pub max_upload_slots_global: i32,
511 #[serde(default = "default_max_upload_slots_per_torrent")]
513 pub max_upload_slots_per_torrent: i32,
514 #[serde(default = "default_max_connections_global")]
516 pub max_connections_global: i32,
517 #[serde(default = "default_max_uploads_per_torrent")]
523 pub max_uploads_per_torrent: i32,
524 #[serde(default)]
526 pub alt_download_rate_limit: u64,
527 #[serde(default)]
529 pub alt_upload_rate_limit: u64,
530 #[serde(default)]
532 pub alt_speed_enabled: bool,
533 #[serde(default)]
535 pub alt_speed_schedule_enabled: bool,
536 #[serde(default)]
538 pub alt_speed_schedule_from: u16,
539 #[serde(default)]
541 pub alt_speed_schedule_to: u16,
542 #[serde(default)]
544 pub alt_speed_schedule_days: u8,
545 #[serde(default = "default_true")]
547 pub rate_limit_includes_overhead: bool,
548 #[serde(default = "default_true")]
550 pub rate_limit_utp: bool,
551 #[serde(default)]
553 pub rate_limit_lan: bool,
554 #[serde(default = "default_mixed_mode")]
556 pub mixed_mode_algorithm: MixedModeAlgorithm,
557
558 #[serde(default = "default_active_downloads")]
561 pub active_downloads: i32,
562 #[serde(default = "default_active_seeds")]
564 pub active_seeds: i32,
565 #[serde(default = "default_active_limit")]
567 pub active_limit: i32,
568 #[serde(default = "default_active_checking")]
570 pub active_checking: i32,
571 #[serde(default = "default_true")]
574 pub dont_count_slow_torrents: bool,
575 #[serde(default = "default_inactive_rate")]
578 pub inactive_down_rate: u64,
579 #[serde(default = "default_inactive_rate")]
582 pub inactive_up_rate: u64,
583 #[serde(default = "default_auto_manage_interval")]
585 pub auto_manage_interval: u64,
586 #[serde(default = "default_auto_manage_startup")]
589 pub auto_manage_startup: u64,
590 #[serde(default)]
592 pub auto_manage_prefer_seeds: bool,
593 #[serde(default = "default_queue_rate_ewma_alpha")]
597 pub queue_rate_ewma_alpha: f64,
598 #[serde(default = "default_seed_queue_min_active_secs")]
602 pub seed_queue_min_active_secs: u64,
603
604 #[serde(default = "default_alert_mask")]
607 pub alert_mask: AlertCategory,
608 #[serde(default = "default_alert_channel_size")]
610 pub alert_channel_size: usize,
611
612 #[serde(default = "default_smart_ban_max_failures")]
616 pub smart_ban_max_failures: u32,
617 #[serde(default = "default_true")]
620 pub smart_ban_parole: bool,
621
622 #[serde(default = "default_disk_io_threads")]
625 pub disk_io_threads: usize,
626 #[serde(default = "default_max_blocking_threads")]
629 pub max_blocking_threads: usize,
630 #[serde(default = "default_storage_mode")]
632 pub storage_mode: StorageMode,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub preallocate_mode: Option<irontide_storage::PreallocateMode>,
637 #[serde(default = "default_disk_cache_size")]
639 pub disk_cache_size: usize,
640 #[serde(default = "default_disk_write_cache_ratio")]
642 pub disk_write_cache_ratio: f32,
643 #[serde(default = "default_disk_channel_capacity")]
645 pub disk_channel_capacity: usize,
646 #[serde(default = "default_buffer_pool_capacity")]
649 pub buffer_pool_capacity: usize,
650 #[serde(default = "default_enable_mlock")]
654 pub enable_mlock: bool,
655 #[serde(default = "default_io_uring_sq_depth")]
658 pub io_uring_sq_depth: u32,
659 #[serde(default)]
662 pub io_uring_direct_io: bool,
663 #[serde(default)]
667 pub filesystem_direct_io: bool,
668 #[serde(default = "default_io_uring_batch_threshold")]
671 pub io_uring_batch_threshold: usize,
672 #[serde(default)]
675 pub iocp_concurrent_threads: u32,
676 #[serde(default)]
679 pub iocp_direct_io: bool,
680 #[serde(default = "default_hashing_threads")]
683 pub hashing_threads: usize,
684 #[serde(default = "default_max_request_queue_depth")]
686 pub max_request_queue_depth: usize,
687 #[serde(default = "default_initial_queue_depth")]
690 pub initial_queue_depth: usize,
691 #[serde(default = "default_request_queue_time")]
698 pub request_queue_time: f64,
699 #[serde(default = "default_block_request_timeout")]
702 pub block_request_timeout_secs: u32,
703 #[serde(default = "default_max_concurrent_streams")]
706 pub max_concurrent_stream_reads: usize,
707 #[serde(default = "default_true")]
710 pub auto_sequential: bool,
711 #[serde(default = "default_strict_end_game")]
715 pub strict_end_game: bool,
716 #[serde(default = "default_max_web_seeds")]
718 pub max_web_seeds: usize,
719 #[serde(default = "default_web_seed_retry_base_secs")]
721 pub web_seed_retry_base_secs: u64,
722 #[serde(default = "default_web_seed_retry_factor")]
724 pub web_seed_retry_factor: u64,
725 #[serde(default = "default_web_seed_retry_cap_secs")]
727 pub web_seed_retry_cap_secs: u64,
728 #[serde(default = "default_web_seed_max_failures")]
730 pub web_seed_max_failures: u32,
731 #[serde(default = "default_initial_picker_threshold")]
734 pub initial_picker_threshold: u32,
735 #[serde(default = "default_whole_pieces_threshold")]
738 pub whole_pieces_threshold: u32,
739 #[serde(default = "default_snub_timeout_secs")]
742 pub snub_timeout_secs: u32,
743 #[serde(default = "default_readahead_pieces")]
745 pub readahead_pieces: u32,
746 #[serde(default = "default_true")]
748 pub streaming_timeout_escalation: bool,
749 #[serde(default = "default_steal_threshold_ratio")]
752 pub steal_threshold_ratio: f64,
753 #[serde(default = "default_use_block_stealing")]
756 pub use_block_stealing: bool,
757 #[serde(default = "default_steal_stale_piece_secs")]
762 pub steal_stale_piece_secs: u64,
763 #[serde(default = "default_steal_threshold_endgame")]
766 pub steal_threshold_endgame: f64,
767 #[serde(default = "default_fixed_pipeline_depth")]
772 pub fixed_pipeline_depth: usize,
773
774 #[serde(default = "default_true")]
778 pub piece_extent_affinity: bool,
779 #[serde(default = "default_suggest_mode")]
783 pub suggest_mode: bool,
784 #[serde(default = "default_max_suggest_pieces")]
786 pub max_suggest_pieces: usize,
787 #[serde(default = "default_predictive_piece_announce_ms")]
791 pub predictive_piece_announce_ms: u64,
792
793 #[serde(default)]
796 pub proxy: ProxyConfig,
797 #[serde(default)]
800 pub force_proxy: bool,
801
802 #[serde(default)]
805 pub ip_filter_enabled: bool,
806 #[serde(default)]
808 pub ip_filter_path: String,
809 #[serde(default)]
811 pub ip_filter_auto_refresh: bool,
812
813 #[serde(default = "default_true")]
816 pub apply_ip_filter_to_trackers: bool,
817
818 #[serde(default = "default_dht_qps")]
821 pub dht_queries_per_second: usize,
822 #[serde(default = "default_dht_timeout")]
824 pub dht_query_timeout_secs: u64,
825 #[serde(default)]
828 pub dht_enforce_node_id: bool,
829 #[serde(default = "default_true")]
831 pub dht_restrict_routing_ips: bool,
832 #[serde(default = "default_dht_max_items")]
834 pub dht_max_items: usize,
835 #[serde(default = "default_dht_item_lifetime")]
837 pub dht_item_lifetime_secs: u64,
838 #[serde(default = "default_dht_sample_interval")]
841 pub dht_sample_infohashes_interval: u64,
842 #[serde(default)]
846 pub dht_read_only: bool,
847
848 #[serde(default = "default_upnp_lease")]
851 pub upnp_lease_duration: u32,
852 #[serde(default = "default_natpmp_lifetime")]
854 pub natpmp_lifetime: u32,
855
856 #[serde(default = "default_utp_max_conns")]
859 pub utp_max_connections: usize,
860
861 #[serde(default)]
864 pub enable_i2p: bool,
865 #[serde(default = "default_i2p_hostname")]
867 pub i2p_hostname: String,
868 #[serde(default = "default_i2p_port")]
870 pub i2p_port: u16,
871 #[serde(default = "default_i2p_tunnel_quantity")]
873 pub i2p_inbound_quantity: u8,
874 #[serde(default = "default_i2p_tunnel_quantity")]
876 pub i2p_outbound_quantity: u8,
877 #[serde(default = "default_i2p_tunnel_length")]
879 pub i2p_inbound_length: u8,
880 #[serde(default = "default_i2p_tunnel_length")]
882 pub i2p_outbound_length: u8,
883 #[serde(default)]
886 pub allow_i2p_mixed: bool,
887
888 #[serde(default = "default_ssl_listen_port")]
893 pub ssl_listen_port: u16,
894 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub ssl_cert_path: Option<PathBuf>,
899 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub ssl_key_path: Option<PathBuf>,
902
903 #[serde(default = "default_seed_choking_algorithm")]
906 pub seed_choking_algorithm: SeedChokingAlgorithm,
907 #[serde(default = "default_choking_algorithm")]
909 pub choking_algorithm: ChokingAlgorithm,
910
911 #[serde(default = "default_max_peers_per_torrent")]
916 pub max_peers_per_torrent: usize,
917
918 #[serde(default = "default_pass0_grace_secs")]
923 pub pass0_grace_secs: u64,
924
925 #[serde(default = "default_proactive_evictions_per_minute_limit")]
930 pub proactive_evictions_per_minute_limit: u32,
931
932 #[serde(default = "default_eviction_ban_duration_secs")]
937 pub eviction_ban_duration_secs: u64,
938
939 #[serde(default = "default_eviction_ban_set_cap")]
943 pub eviction_ban_set_cap: usize,
944
945 #[serde(default = "default_peer_read_timeout_secs")]
948 pub peer_read_timeout_secs: u64,
949 #[serde(default = "default_peer_write_timeout_secs")]
952 pub peer_write_timeout_secs: u64,
953
954 #[serde(default = "default_data_contribution_timeout")]
957 pub data_contribution_timeout_secs: u64,
958
959 #[serde(default = "default_choke_rotation_max_evictions")]
961 pub choke_rotation_max_evictions: u32,
962
963 #[serde(default = "default_max_concurrent_connects")]
965 pub max_concurrent_connects: u16,
966
967 #[serde(default = "default_connect_soft_timeout")]
970 pub connect_soft_timeout: u64,
971
972 #[serde(default = "default_dispatch_backlog_cap")]
978 pub dispatch_backlog_cap: usize,
979
980 #[serde(default = "default_event_backlog_cap")]
984 pub event_backlog_cap: usize,
985
986 #[serde(default = "default_true")]
988 pub use_actor_dispatch: bool,
989
990 #[serde(default = "default_web_seed_progress_throttle_ms")]
995 pub web_seed_progress_throttle_ms: u64,
996
997 #[serde(default = "default_true")]
1001 pub ssrf_mitigation: bool,
1002 #[serde(default)]
1004 pub allow_idna: bool,
1005 #[serde(default = "default_true")]
1007 pub validate_https_trackers: bool,
1008 #[serde(default = "default_max_metadata_size")]
1011 pub max_metadata_size: u64,
1012 #[serde(default = "default_max_message_size")]
1015 pub max_message_size: usize,
1016 #[serde(default = "default_max_piece_length")]
1019 pub max_piece_length: u64,
1020 #[serde(default = "default_max_outstanding_requests")]
1024 pub max_outstanding_requests: usize,
1025 #[serde(default = "default_max_in_flight_pieces")]
1030 pub max_in_flight_pieces: usize,
1031 #[serde(default = "default_peer_connect_timeout")]
1034 pub peer_connect_timeout: u64,
1035 #[serde(default = "default_peer_dscp")]
1039 pub peer_dscp: u8,
1040
1041 #[serde(default = "default_stats_report_interval")]
1045 pub stats_report_interval: u64,
1046
1047 #[serde(default = "default_runtime_worker_threads")]
1051 pub runtime_worker_threads: usize,
1052 #[serde(default = "default_true")]
1054 pub pin_cores: bool,
1055
1056 #[serde(default = "default_lock_warn_threshold_ms")]
1062 pub lock_warn_threshold_ms: u64,
1063
1064 #[serde(skip)]
1070 pub dht_saved_nodes: Vec<String>,
1071 #[serde(skip)]
1075 pub dht_node_id: Option<irontide_core::Id20>,
1076
1077 #[serde(default)]
1081 pub qbt_compat: QbtCompatSettings,
1082
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1087 pub category_registry_path: Option<PathBuf>,
1088
1089 #[serde(default, skip_serializing_if = "Option::is_none")]
1093 pub tag_registry_path: Option<PathBuf>,
1094
1095 #[serde(default)]
1100 pub notify_on_complete: bool,
1101 #[serde(default)]
1104 pub notify_on_error: bool,
1105 #[serde(default, skip_serializing_if = "Option::is_none")]
1110 pub on_complete_program: Option<PathBuf>,
1111 #[serde(default)]
1115 pub use_incomplete_dir: bool,
1116 #[serde(default, skip_serializing_if = "Option::is_none")]
1120 pub incomplete_dir: Option<PathBuf>,
1121 #[serde(default)]
1125 pub default_skip_hash_check: bool,
1126 #[serde(default = "default_true")]
1130 pub incomplete_extension_enabled: bool,
1131 #[serde(default, skip_serializing_if = "Option::is_none")]
1135 pub watched_folder: Option<PathBuf>,
1136 #[serde(default)]
1141 pub delete_torrent_after_add: bool,
1142 #[serde(default)]
1146 pub move_completed_enabled: bool,
1147 #[serde(default, skip_serializing_if = "Option::is_none")]
1151 pub move_completed_to: Option<PathBuf>,
1152 #[serde(default)]
1156 pub web_ui_https_enabled: bool,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub network_interface: Option<String>,
1162 #[serde(default)]
1167 pub default_add_paused: bool,
1168}
1169
1170#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
1188#[serde(default)]
1189pub struct QbtCompatSettings {
1190 pub enabled: bool,
1198 pub username: String,
1201 pub password_hash: String,
1209 #[serde(default)]
1215 pub password: String,
1216 pub spoof_app_version: String,
1219 pub spoof_webapi_version: String,
1222 pub session_ttl_secs: u64,
1225 pub max_sessions: usize,
1228 #[serde(default, skip_serializing_if = "Option::is_none")]
1233 pub max_concurrent_argon2_ops: Option<u32>,
1234
1235 #[serde(default = "default_qbt_port")]
1240 pub port: u16,
1241
1242 #[serde(default = "default_qbt_bind_address")]
1247 pub bind_address: String,
1248
1249 #[serde(default = "default_csrf_protection_enabled")]
1256 pub csrf_protection_enabled: bool,
1257 #[serde(default = "default_host_header_validation_enabled")]
1263 pub host_header_validation_enabled: bool,
1264 #[serde(default)]
1272 pub web_ui_reverse_proxy_enabled: bool,
1273 #[serde(default)]
1284 pub web_ui_reverse_proxies_list: Vec<String>,
1285
1286 #[serde(default = "default_max_failed_auth_count")]
1293 pub max_failed_auth_count: u32,
1294 #[serde(default = "default_ban_duration_secs")]
1297 pub ban_duration_secs: u64,
1298 #[serde(default)]
1306 pub bypass_local_auth: bool,
1307 #[serde(default)]
1315 pub bypass_auth_subnet_whitelist: Vec<String>,
1316 #[serde(default, skip_serializing_if = "Option::is_none")]
1324 pub brute_force_registry_capacity: Option<usize>,
1325}
1326
1327fn default_csrf_protection_enabled() -> bool {
1328 true
1329}
1330
1331fn default_host_header_validation_enabled() -> bool {
1332 true
1333}
1334
1335fn default_qbt_port() -> u16 {
1338 9080
1339}
1340
1341fn default_qbt_bind_address() -> String {
1342 "127.0.0.1".to_owned()
1343}
1344
1345#[must_use]
1347pub const fn default_max_failed_auth_count() -> u32 {
1348 5
1349}
1350
1351#[must_use]
1353pub const fn default_ban_duration_secs() -> u64 {
1354 3_600
1355}
1356
1357pub const DEFAULT_ADMINADMIN_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$u3doPIM7ab7NlbMfhMFm6A$ctIAjFfl70eUfUsThdGcXICr0lcD6bEUilRujvnXLPg";
1368
1369impl Default for QbtCompatSettings {
1370 fn default() -> Self {
1371 Self {
1372 enabled: true,
1373 username: "admin".into(),
1374 password_hash: DEFAULT_ADMINADMIN_HASH.into(),
1375 password: String::new(),
1376 spoof_app_version: "v5.1.4".into(),
1377 spoof_webapi_version: "2.11.4".into(),
1378 session_ttl_secs: 86_400,
1379 max_sessions: 1024,
1380 max_concurrent_argon2_ops: None,
1381 port: default_qbt_port(),
1383 bind_address: default_qbt_bind_address(),
1384 csrf_protection_enabled: true,
1386 host_header_validation_enabled: true,
1387 web_ui_reverse_proxy_enabled: false,
1388 web_ui_reverse_proxies_list: Vec::new(),
1389 max_failed_auth_count: default_max_failed_auth_count(),
1391 ban_duration_secs: default_ban_duration_secs(),
1392 bypass_local_auth: false,
1393 bypass_auth_subnet_whitelist: Vec::new(),
1394 brute_force_registry_capacity: None,
1395 }
1396 }
1397}
1398
1399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1401pub enum QbtCredentialMigration {
1402 NoOp,
1404 Upgraded,
1408}
1409
1410pub fn hash_qbt_password(plaintext: &str) -> Result<String, QbtMigrationError> {
1422 use argon2::password_hash::{PasswordHasher, SaltString};
1423 use argon2::{Algorithm, Argon2, Params, Version};
1424
1425 let salt = SaltString::generate(&mut rand_core::OsRng);
1429 let params = Params::new(19_456, 2, 1, Some(32))
1432 .map_err(|e| QbtMigrationError::Hash(format!("argon2 params: {e}")))?;
1433 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
1434 let hash = argon2
1435 .hash_password(plaintext.as_bytes(), &salt)
1436 .map_err(|e| QbtMigrationError::Hash(format!("argon2 hash: {e}")))?;
1437 Ok(hash.to_string())
1438}
1439
1440#[derive(Debug, thiserror::Error)]
1442pub enum QbtMigrationError {
1443 #[error("argon2 hash: {0}")]
1445 Hash(String),
1446}
1447
1448pub fn migrate_qbt_credentials(
1475 qbt: &mut QbtCompatSettings,
1476) -> Result<QbtCredentialMigration, QbtMigrationError> {
1477 if !qbt.password_hash.is_empty() {
1478 return Ok(QbtCredentialMigration::NoOp);
1479 }
1480 if qbt.password.is_empty() {
1481 return Ok(QbtCredentialMigration::NoOp);
1482 }
1483
1484 let hash = hash_qbt_password(&qbt.password)?;
1485 qbt.password_hash = hash;
1486 let _drain = zeroize::Zeroizing::new(std::mem::take(&mut qbt.password));
1490 Ok(QbtCredentialMigration::Upgraded)
1491}
1492
1493fn is_valid_app_version(s: &str) -> bool {
1495 let Some(rest) = s.strip_prefix('v') else {
1496 return false;
1497 };
1498 let (core, suffix_ok) = match rest.split_once('-') {
1500 Some((core, suffix)) => (
1501 core,
1502 !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_alphanumeric()),
1503 ),
1504 None => (rest, true),
1505 };
1506 if !suffix_ok {
1507 return false;
1508 }
1509 is_valid_dotted_numeric(core)
1510}
1511
1512fn is_valid_webapi_version(s: &str) -> bool {
1514 is_valid_dotted_numeric(s)
1515}
1516
1517fn is_valid_dotted_numeric(s: &str) -> bool {
1519 let parts: Vec<&str> = s.split('.').collect();
1520 if !(2..=3).contains(&parts.len()) {
1521 return false;
1522 }
1523 parts
1524 .iter()
1525 .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
1526}
1527
1528impl Default for Settings {
1529 fn default() -> Self {
1530 Self {
1531 listen_port: 42020,
1533 randomize_port_on_startup: false,
1534 download_dir: PathBuf::from("."),
1535 max_torrents: 100,
1536 resume_data_dir: None,
1537 save_resume_interval_secs: 300,
1538 enable_dht: true,
1540 enable_pex: true,
1541 enable_lsd: true,
1542 enable_fast_extension: true,
1543 enable_utp: true,
1544 enable_upnp: true,
1545 enable_natpmp: true,
1546 enable_ipv6: true,
1547 enable_web_seed: true,
1548 enable_holepunch: true,
1549 enable_bep40_eviction: true,
1550 enable_diagnostic_counters: false,
1551 encryption_mode: EncryptionMode::Disabled,
1552 anonymous_mode: false,
1553 external_ip: None,
1554 seed_ratio_limit: None,
1556 seed_time_limit_secs: None,
1557 inactive_seed_time_limit_secs: None,
1558 max_ratio_action: MaxRatioAction::Pause,
1559 create_subfolder: true,
1560 auto_manage_torrents: false,
1561 queueing_enabled: false,
1562 default_super_seeding: false,
1563 default_share_mode: false,
1564 upload_only_announce: true,
1565 upload_rate_limit: 0,
1567 download_rate_limit: 0,
1568 tcp_upload_rate_limit: 0,
1569 tcp_download_rate_limit: 0,
1570 utp_upload_rate_limit: 0,
1571 utp_download_rate_limit: 0,
1572 auto_upload_slots: true,
1573 auto_upload_slots_min: 2,
1574 auto_upload_slots_max: 20,
1575 max_upload_slots_global: -1,
1576 max_upload_slots_per_torrent: 4,
1577 max_connections_global: -1,
1578 max_uploads_per_torrent: -1,
1579 alt_download_rate_limit: 0,
1580 alt_upload_rate_limit: 0,
1581 alt_speed_enabled: false,
1582 alt_speed_schedule_enabled: false,
1583 alt_speed_schedule_from: 0,
1584 alt_speed_schedule_to: 0,
1585 alt_speed_schedule_days: 0,
1586 rate_limit_includes_overhead: true,
1587 rate_limit_utp: true,
1588 rate_limit_lan: false,
1589 mixed_mode_algorithm: MixedModeAlgorithm::PeerProportional,
1590 active_downloads: 3,
1592 active_seeds: 5,
1593 active_limit: 500,
1594 active_checking: 3,
1595 dont_count_slow_torrents: true,
1596 inactive_down_rate: 2048,
1597 inactive_up_rate: 2048,
1598 auto_manage_interval: 30,
1599 auto_manage_startup: 60,
1600 auto_manage_prefer_seeds: false,
1601 queue_rate_ewma_alpha: 0.3,
1602 seed_queue_min_active_secs: 1800,
1603 alert_mask: AlertCategory::ALL,
1605 alert_channel_size: 1024,
1606 smart_ban_max_failures: 3,
1608 smart_ban_parole: true,
1609 disk_io_threads: default_disk_io_threads(),
1611 max_blocking_threads: default_max_blocking_threads(),
1612 storage_mode: StorageMode::Auto,
1613 preallocate_mode: None,
1614 disk_cache_size: 16 * 1024 * 1024,
1615 disk_write_cache_ratio: 0.5,
1616 disk_channel_capacity: 512,
1617 buffer_pool_capacity: 64 * 1024 * 1024,
1618 enable_mlock: cfg!(unix),
1619 io_uring_sq_depth: 256,
1620 io_uring_direct_io: false,
1621 filesystem_direct_io: false,
1622 io_uring_batch_threshold: 4,
1623 iocp_concurrent_threads: 0,
1624 iocp_direct_io: false,
1625 hashing_threads: default_hashing_threads(),
1627 max_request_queue_depth: 250,
1628 initial_queue_depth: 128,
1629 request_queue_time: 3.0,
1630 block_request_timeout_secs: 60,
1631 max_concurrent_stream_reads: 8,
1632 auto_sequential: true,
1633 steal_threshold_ratio: 10.0,
1634 use_block_stealing: true,
1635 steal_stale_piece_secs: 2,
1636 steal_threshold_endgame: 3.0,
1637 fixed_pipeline_depth: 128,
1638 strict_end_game: true,
1639 max_web_seeds: 4,
1640 web_seed_retry_base_secs: 10,
1641 web_seed_retry_factor: 6,
1642 web_seed_retry_cap_secs: 3600,
1643 web_seed_max_failures: 10,
1644 initial_picker_threshold: 4,
1645 whole_pieces_threshold: 20,
1646 snub_timeout_secs: 15,
1647 readahead_pieces: 8,
1648 streaming_timeout_escalation: true,
1649 piece_extent_affinity: true,
1651 suggest_mode: true,
1652 max_suggest_pieces: 16,
1653 predictive_piece_announce_ms: 0,
1654 proxy: ProxyConfig::default(),
1656 force_proxy: false,
1657 ip_filter_enabled: false,
1659 ip_filter_path: String::new(),
1660 ip_filter_auto_refresh: false,
1661 apply_ip_filter_to_trackers: true,
1662 dht_queries_per_second: 50,
1664 dht_query_timeout_secs: 5,
1665 dht_enforce_node_id: false,
1666 dht_restrict_routing_ips: true,
1667 dht_max_items: 700,
1668 dht_item_lifetime_secs: 7200,
1669 dht_sample_infohashes_interval: 0,
1670 dht_read_only: false,
1671 upnp_lease_duration: 3600,
1673 natpmp_lifetime: 7200,
1674 utp_max_connections: 256,
1676 enable_i2p: false,
1678 i2p_hostname: "127.0.0.1".into(),
1679 i2p_port: 7656,
1680 i2p_inbound_quantity: 3,
1681 i2p_outbound_quantity: 3,
1682 i2p_inbound_length: 3,
1683 i2p_outbound_length: 3,
1684 allow_i2p_mixed: false,
1685 ssl_listen_port: 0,
1687 ssl_cert_path: None,
1688 ssl_key_path: None,
1689 seed_choking_algorithm: SeedChokingAlgorithm::FastestUpload,
1691 choking_algorithm: ChokingAlgorithm::FixedSlots,
1692 max_peers_per_torrent: 128,
1694 pass0_grace_secs: 60,
1696 proactive_evictions_per_minute_limit: 30,
1697 eviction_ban_duration_secs: 600,
1698 eviction_ban_set_cap: 1024,
1699 peer_read_timeout_secs: 10,
1700 peer_write_timeout_secs: 10,
1701 data_contribution_timeout_secs: 0,
1702 choke_rotation_max_evictions: 0,
1703 max_concurrent_connects: 128,
1704 connect_soft_timeout: 3,
1705 dispatch_backlog_cap: 8,
1706 event_backlog_cap: 32,
1707 use_actor_dispatch: true,
1708 web_seed_progress_throttle_ms: 250,
1709 ssrf_mitigation: true,
1711 allow_idna: false,
1712 validate_https_trackers: true,
1713 max_metadata_size: 4 * 1024 * 1024,
1714 max_message_size: 16 * 1024 * 1024,
1715 max_piece_length: 32 * 1024 * 1024,
1716 max_outstanding_requests: 500,
1717 max_in_flight_pieces: 512,
1718 peer_connect_timeout: 10,
1719 peer_dscp: 0x08,
1720 stats_report_interval: 1000,
1722 runtime_worker_threads: default_runtime_worker_threads(),
1724 pin_cores: true,
1725 lock_warn_threshold_ms: 50,
1727 dht_saved_nodes: Vec::new(),
1729 dht_node_id: None,
1730 qbt_compat: QbtCompatSettings::default(),
1732 category_registry_path: None,
1735 tag_registry_path: None,
1738 notify_on_complete: false,
1740 notify_on_error: false,
1741 on_complete_program: None,
1742 use_incomplete_dir: false,
1743 incomplete_dir: None,
1744 default_skip_hash_check: false,
1745 incomplete_extension_enabled: true,
1746 watched_folder: None,
1747 delete_torrent_after_add: false,
1748 move_completed_enabled: false,
1749 move_completed_to: None,
1750 web_ui_https_enabled: false,
1751 network_interface: None,
1752 default_add_paused: false,
1753 }
1754 }
1755}
1756
1757impl Settings {
1758 #[must_use]
1760 pub fn min_memory() -> Self {
1761 Self {
1762 disk_cache_size: 8 * 1024 * 1024,
1763 buffer_pool_capacity: 16 * 1024 * 1024,
1764 max_torrents: 20,
1765 max_peers_per_torrent: 30,
1766 active_downloads: 1,
1767 active_seeds: 2,
1768 active_limit: 10,
1769 alert_channel_size: 256,
1770 utp_max_connections: 64,
1771 max_request_queue_depth: 50,
1772 initial_queue_depth: 16,
1773 max_concurrent_stream_reads: 2,
1774 hashing_threads: 1,
1775 disk_io_threads: 1,
1776 dht_max_items: 100,
1777 max_in_flight_pieces: 32,
1778 fixed_pipeline_depth: 32,
1779 ..Self::default()
1780 }
1781 }
1782
1783 #[must_use]
1785 pub fn high_performance() -> Self {
1786 Self {
1787 disk_cache_size: 256 * 1024 * 1024,
1788 buffer_pool_capacity: 256 * 1024 * 1024,
1789 max_torrents: 2000,
1790 max_peers_per_torrent: 200,
1791 active_downloads: 30,
1792 active_seeds: 100,
1793 active_limit: 2000,
1794 alert_channel_size: 4096,
1795 utp_max_connections: 1024,
1796 max_request_queue_depth: 1000,
1797 initial_queue_depth: 256,
1798 max_concurrent_stream_reads: 32,
1799 hashing_threads: 4,
1800 disk_io_threads: 8,
1801 auto_upload_slots_max: 100,
1802 suggest_mode: true,
1803 steal_threshold_ratio: 5.0,
1804 steal_threshold_endgame: 2.0,
1805 use_block_stealing: true,
1806 max_in_flight_pieces: 512,
1807 ..Self::default()
1808 }
1809 }
1810
1811 pub fn validate(&self) -> crate::Result<()> {
1817 use crate::proxy::ProxyType;
1818
1819 if self.force_proxy && self.proxy.proxy_type == ProxyType::None {
1820 return Err(crate::Error::InvalidSettings(
1821 "force_proxy is enabled but no proxy type is configured".into(),
1822 ));
1823 }
1824
1825 if self.active_downloads > 0
1826 && self.active_limit > 0
1827 && self.active_downloads > self.active_limit
1828 {
1829 return Err(crate::Error::InvalidSettings(
1830 "active_downloads exceeds active_limit".into(),
1831 ));
1832 }
1833
1834 if self.active_seeds > 0 && self.active_limit > 0 && self.active_seeds > self.active_limit {
1835 return Err(crate::Error::InvalidSettings(
1836 "active_seeds exceeds active_limit".into(),
1837 ));
1838 }
1839
1840 if !(0.0..=1.0).contains(&self.disk_write_cache_ratio) {
1841 return Err(crate::Error::InvalidSettings(
1842 "disk_write_cache_ratio must be between 0.0 and 1.0".into(),
1843 ));
1844 }
1845
1846 if self.disk_cache_size < 1024 * 1024 {
1847 return Err(crate::Error::InvalidSettings(
1848 "disk_cache_size must be at least 1 MiB".into(),
1849 ));
1850 }
1851
1852 if self.hashing_threads == 0 {
1853 return Err(crate::Error::InvalidSettings(
1854 "hashing_threads must be at least 1".into(),
1855 ));
1856 }
1857
1858 if self.use_incomplete_dir && self.incomplete_dir.is_none() {
1860 return Err(crate::Error::InvalidSettings(
1861 "incomplete_dir must be set when use_incomplete_dir=true".into(),
1862 ));
1863 }
1864 if self.move_completed_enabled && self.move_completed_to.is_none() {
1865 return Err(crate::Error::InvalidSettings(
1866 "move_completed_to must be set when move_completed_enabled=true".into(),
1867 ));
1868 }
1869 for (name, opt) in [
1872 ("watched_folder", self.watched_folder.as_ref()),
1873 ("incomplete_dir", self.incomplete_dir.as_ref()),
1874 ("move_completed_to", self.move_completed_to.as_ref()),
1875 ] {
1876 if let Some(p) = opt
1877 && !p.is_absolute()
1878 {
1879 return Err(crate::Error::InvalidSettings(format!(
1880 "{name} must be an absolute path, got {}",
1881 p.display()
1882 )));
1883 }
1884 }
1885 if let Some(p) = self.watched_folder.as_ref() {
1888 const DENY: &[&str] = &[
1889 "/", "/etc", "/usr", "/bin", "/sbin", "/lib", "/lib64", "/boot", "/sys",
1890 "/proc", "/dev", "/run", "/var/lib", "/var/log",
1891 ];
1892 let s = p.to_string_lossy();
1893 if DENY.iter().any(|d| s == *d) {
1894 return Err(crate::Error::InvalidSettings(format!(
1895 "watched_folder rejected: {} is a system path (would risk shredding system files if delete_torrent_after_add=true)",
1896 p.display()
1897 )));
1898 }
1899 if let Some(home) = std::env::var_os("HOME") {
1900 let home_path = PathBuf::from(home);
1901 if p == &home_path {
1902 return Err(crate::Error::InvalidSettings(format!(
1903 "watched_folder cannot be $HOME ({}) — too broad to be a torrent dropbox; pick a dedicated subdirectory",
1904 p.display()
1905 )));
1906 }
1907 }
1908 }
1909
1910 if self.max_uploads_per_torrent == 0 || self.max_uploads_per_torrent < -1 {
1916 return Err(crate::Error::InvalidSettings(
1917 "max_uploads_per_torrent must be -1 (unlimited) or >= 1".into(),
1918 ));
1919 }
1920
1921 if self.disk_io_threads == 0 {
1922 return Err(crate::Error::InvalidSettings(
1923 "disk_io_threads must be at least 1".into(),
1924 ));
1925 }
1926
1927 if self.max_blocking_threads == 0 {
1928 return Err(crate::Error::InvalidSettings(
1929 "max_blocking_threads must be at least 1".into(),
1930 ));
1931 }
1932
1933 if self.default_share_mode && !self.enable_fast_extension {
1934 return Err(crate::Error::InvalidSettings(
1935 "share_mode requires enable_fast_extension for RejectRequest messages".into(),
1936 ));
1937 }
1938
1939 if self.ssl_cert_path.is_some() != self.ssl_key_path.is_some() {
1941 return Err(crate::Error::InvalidSettings(
1942 "ssl_cert_path and ssl_key_path must both be set or both absent".into(),
1943 ));
1944 }
1945
1946 if self.enable_i2p {
1947 if self.i2p_inbound_quantity == 0 || self.i2p_inbound_quantity > 16 {
1948 return Err(crate::Error::InvalidSettings(
1949 "i2p_inbound_quantity must be 1-16".into(),
1950 ));
1951 }
1952 if self.i2p_outbound_quantity == 0 || self.i2p_outbound_quantity > 16 {
1953 return Err(crate::Error::InvalidSettings(
1954 "i2p_outbound_quantity must be 1-16".into(),
1955 ));
1956 }
1957 if self.i2p_inbound_length > 7 {
1958 return Err(crate::Error::InvalidSettings(
1959 "i2p_inbound_length must be 0-7".into(),
1960 ));
1961 }
1962 if self.i2p_outbound_length > 7 {
1963 return Err(crate::Error::InvalidSettings(
1964 "i2p_outbound_length must be 0-7".into(),
1965 ));
1966 }
1967 }
1968
1969 if self.runtime_worker_threads > 256 {
1970 return Err(crate::Error::InvalidSettings(
1971 "runtime_worker_threads must be at most 256".into(),
1972 ));
1973 }
1974
1975 if self.qbt_compat.enabled {
1979 if self.qbt_compat.username.is_empty() {
1980 return Err(crate::Error::InvalidSettings(
1981 "qbt_compat.username must not be empty when enabled".into(),
1982 ));
1983 }
1984 if self.qbt_compat.password_hash.is_empty() {
1988 if self.qbt_compat.password.len() < 8 {
1989 return Err(crate::Error::InvalidSettings(
1990 "qbt_compat: either password_hash must be set OR \
1991 password must be at least 8 characters (legacy upgrade path)"
1992 .into(),
1993 ));
1994 }
1995 } else if !self.qbt_compat.password_hash.starts_with("$argon2id$") {
1996 return Err(crate::Error::InvalidSettings(
2000 "qbt_compat.password_hash must be an argon2id PHC string \
2001 starting with `$argon2id$`"
2002 .into(),
2003 ));
2004 }
2005 if let Some(0) = self.qbt_compat.max_concurrent_argon2_ops {
2006 return Err(crate::Error::InvalidSettings(
2007 "qbt_compat.max_concurrent_argon2_ops must be > 0 when set".into(),
2008 ));
2009 }
2010 if !is_valid_app_version(&self.qbt_compat.spoof_app_version) {
2011 return Err(crate::Error::InvalidSettings(
2012 "qbt_compat.spoof_app_version must match vN.N[.N][-suffix] (e.g. v5.1.4)"
2013 .into(),
2014 ));
2015 }
2016 if !is_valid_webapi_version(&self.qbt_compat.spoof_webapi_version) {
2017 return Err(crate::Error::InvalidSettings(
2018 "qbt_compat.spoof_webapi_version must match N.N[.N] (e.g. 2.11.4)".into(),
2019 ));
2020 }
2021 if !(60..=604_800).contains(&self.qbt_compat.session_ttl_secs) {
2022 return Err(crate::Error::InvalidSettings(
2023 "qbt_compat.session_ttl_secs must be in [60, 604800]".into(),
2024 ));
2025 }
2026 if self.qbt_compat.max_sessions == 0 {
2027 return Err(crate::Error::InvalidSettings(
2028 "qbt_compat.max_sessions must be at least 1".into(),
2029 ));
2030 }
2031 for entry in &self.qbt_compat.web_ui_reverse_proxies_list {
2036 if entry.parse::<ipnet::IpNet>().is_err() {
2037 return Err(crate::Error::InvalidSettings(format!(
2038 "qbt_compat.web_ui_reverse_proxies_list: invalid CIDR '{entry}'"
2039 )));
2040 }
2041 }
2042
2043 if self.qbt_compat.max_failed_auth_count == 0 && !self.qbt_compat.bypass_local_auth {
2049 return Err(crate::Error::InvalidSettings(
2050 "qbt_compat.max_failed_auth_count must be > 0 when bypass_local_auth is false"
2051 .into(),
2052 ));
2053 }
2054 if !(60..=86_400).contains(&self.qbt_compat.ban_duration_secs) {
2055 return Err(crate::Error::InvalidSettings(
2056 "qbt_compat.ban_duration_secs must be in [60, 86400]".into(),
2057 ));
2058 }
2059 for cidr in &self.qbt_compat.bypass_auth_subnet_whitelist {
2060 if cidr.parse::<ipnet::IpNet>().is_err() {
2061 return Err(crate::Error::InvalidSettings(format!(
2062 "qbt_compat.bypass_auth_subnet_whitelist: invalid CIDR `{cidr}`"
2063 )));
2064 }
2065 }
2066 if let Some(cap) = self.qbt_compat.brute_force_registry_capacity
2067 && cap < 100
2068 {
2069 return Err(crate::Error::InvalidSettings(
2070 "qbt_compat.brute_force_registry_capacity must be at least 100".into(),
2071 ));
2072 }
2073 }
2074
2075 Ok(())
2076 }
2077}
2078
2079impl From<&Settings> for crate::disk::DiskConfig {
2082 fn from(s: &Settings) -> Self {
2083 Self {
2084 io_threads: s.disk_io_threads,
2085 storage_mode: s.storage_mode,
2086 cache_size: s.disk_cache_size,
2087 write_cache_ratio: s.disk_write_cache_ratio,
2088 channel_capacity: s.disk_channel_capacity,
2089 buffer_pool_capacity: s.buffer_pool_capacity,
2090 enable_mlock: s.enable_mlock,
2091 lock_warn_threshold_ms: s.lock_warn_threshold_ms,
2092 io_uring_sq_depth: s.io_uring_sq_depth,
2093 io_uring_direct_io: s.io_uring_direct_io,
2094 filesystem_direct_io: s.filesystem_direct_io,
2095 io_uring_batch_threshold: s.io_uring_batch_threshold,
2096 iocp_concurrent_threads: s.iocp_concurrent_threads,
2097 iocp_direct_io: s.iocp_direct_io,
2098 }
2099 }
2100}
2101
2102impl From<&Settings> for crate::ban::BanConfig {
2103 fn from(s: &Settings) -> Self {
2104 Self {
2105 max_failures: s.smart_ban_max_failures,
2106 use_parole: s.smart_ban_parole,
2107 }
2108 }
2109}
2110
2111impl Settings {
2112 pub(crate) fn to_dht_config(&self) -> irontide_dht::DhtConfig {
2113 let default = irontide_dht::DhtConfig::default();
2114 let mut bootstrap = self.dht_saved_nodes.clone();
2115 bootstrap.extend(default.bootstrap_nodes.iter().cloned());
2116 irontide_dht::DhtConfig {
2117 bootstrap_nodes: bootstrap,
2118 own_id: self.dht_node_id,
2119 queries_per_second: self.dht_queries_per_second,
2120 query_timeout: std::time::Duration::from_secs(self.dht_query_timeout_secs),
2121 enforce_node_id: self.dht_enforce_node_id,
2122 restrict_routing_ips: self.dht_restrict_routing_ips,
2123 dht_max_items: self.dht_max_items,
2124 dht_item_lifetime_secs: self.dht_item_lifetime_secs,
2125 state_dir: self.resume_data_dir.clone(),
2126 read_only_mode: self.dht_read_only,
2127 ..default
2128 }
2129 }
2130
2131 pub(crate) fn to_dht_config_v6(&self) -> irontide_dht::DhtConfig {
2132 let default = irontide_dht::DhtConfig::default_v6();
2133 let mut bootstrap = self.dht_saved_nodes.clone();
2134 bootstrap.extend(default.bootstrap_nodes.iter().cloned());
2135 irontide_dht::DhtConfig {
2136 bootstrap_nodes: bootstrap,
2137 queries_per_second: self.dht_queries_per_second,
2138 query_timeout: std::time::Duration::from_secs(self.dht_query_timeout_secs),
2139 enforce_node_id: self.dht_enforce_node_id,
2140 restrict_routing_ips: self.dht_restrict_routing_ips,
2141 dht_max_items: self.dht_max_items,
2142 dht_item_lifetime_secs: self.dht_item_lifetime_secs,
2143 state_dir: self.resume_data_dir.clone(),
2144 read_only_mode: self.dht_read_only,
2145 ..default
2146 }
2147 }
2148
2149 pub(crate) fn to_nat_config(&self) -> irontide_nat::NatConfig {
2150 irontide_nat::NatConfig {
2151 enable_upnp: self.enable_upnp,
2152 enable_natpmp: self.enable_natpmp,
2153 upnp_lease_duration: self.upnp_lease_duration,
2154 natpmp_lifetime: self.natpmp_lifetime,
2155 }
2156 }
2157
2158 pub(crate) fn to_utp_config(&self, port: u16) -> irontide_utp::UtpConfig {
2159 irontide_utp::UtpConfig {
2160 bind_addr: std::net::SocketAddr::from(([0, 0, 0, 0], port)),
2161 max_connections: self.utp_max_connections,
2162 dscp: self.peer_dscp,
2163 }
2164 }
2165
2166 pub(crate) fn to_utp_config_v6(&self, port: u16) -> irontide_utp::UtpConfig {
2167 irontide_utp::UtpConfig {
2168 bind_addr: std::net::SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, port)),
2169 max_connections: self.utp_max_connections,
2170 dscp: self.peer_dscp,
2171 }
2172 }
2173
2174 pub(crate) fn to_sam_tunnel_config(&self) -> crate::i2p::SamTunnelConfig {
2176 crate::i2p::SamTunnelConfig {
2177 inbound_quantity: self.i2p_inbound_quantity,
2178 outbound_quantity: self.i2p_outbound_quantity,
2179 inbound_length: self.i2p_inbound_length,
2180 outbound_length: self.i2p_outbound_length,
2181 }
2182 }
2183}
2184
2185#[cfg(test)]
2191mod tests {
2192 use super::*;
2193
2194 #[test]
2195 fn default_settings_values() {
2196 let s = Settings::default();
2197 assert_eq!(s.listen_port, 42020);
2198 assert_eq!(s.download_dir, PathBuf::from("."));
2199 assert_eq!(s.max_torrents, 100);
2200 assert!(s.resume_data_dir.is_none());
2201 assert_eq!(s.save_resume_interval_secs, 300);
2202 assert!(s.enable_dht);
2203 assert!(s.enable_pex);
2204 assert!(s.enable_lsd);
2205 assert!(s.enable_fast_extension);
2206 assert!(s.enable_utp);
2207 assert!(s.enable_upnp);
2208 assert!(s.enable_natpmp);
2209 assert!(s.enable_ipv6);
2210 assert!(s.enable_web_seed);
2211 assert_eq!(s.encryption_mode, EncryptionMode::Disabled);
2212 assert!(!s.anonymous_mode);
2213 assert!(s.seed_ratio_limit.is_none());
2214 assert!(!s.default_super_seeding);
2215 assert!(!s.default_share_mode);
2216 assert!(s.upload_only_announce);
2217 assert_eq!(s.upload_rate_limit, 0);
2218 assert_eq!(s.download_rate_limit, 0);
2219 assert!(s.auto_upload_slots);
2220 assert_eq!(s.active_downloads, 3);
2221 assert_eq!(s.active_seeds, 5);
2222 assert_eq!(s.active_limit, 500);
2223 assert_eq!(s.active_checking, 3);
2224 assert!(s.dont_count_slow_torrents);
2225 assert_eq!(s.alert_mask, AlertCategory::ALL);
2226 assert_eq!(s.alert_channel_size, 1024);
2227 assert_eq!(s.smart_ban_max_failures, 3);
2228 assert!(s.smart_ban_parole);
2229 assert_eq!(s.disk_io_threads, default_disk_io_threads());
2230 assert_eq!(s.max_blocking_threads, default_max_blocking_threads());
2231 assert_eq!(s.storage_mode, StorageMode::Auto);
2232 assert_eq!(s.disk_cache_size, 16 * 1024 * 1024);
2233 assert!((s.disk_write_cache_ratio - 0.5).abs() < f32::EPSILON);
2234 assert_eq!(s.disk_channel_capacity, 512);
2235 assert_eq!(s.hashing_threads, default_hashing_threads());
2236 assert_eq!(s.max_request_queue_depth, 250);
2237 assert_eq!(s.initial_queue_depth, 128);
2238 assert!((s.request_queue_time - 3.0).abs() < f64::EPSILON);
2239 assert_eq!(s.block_request_timeout_secs, 60);
2240 assert_eq!(s.max_concurrent_stream_reads, 8);
2241 assert!(!s.force_proxy);
2242 assert!(s.apply_ip_filter_to_trackers);
2243 assert_eq!(s.dht_queries_per_second, 50);
2244 assert_eq!(s.dht_query_timeout_secs, 5);
2245 assert!(!s.dht_enforce_node_id);
2246 assert!(s.dht_restrict_routing_ips);
2247 assert_eq!(s.upnp_lease_duration, 3600);
2248 assert_eq!(s.natpmp_lifetime, 7200);
2249 assert_eq!(s.utp_max_connections, 256);
2250 assert_eq!(s.mixed_mode_algorithm, MixedModeAlgorithm::PeerProportional);
2251 assert!(s.auto_sequential);
2252 assert!(s.strict_end_game);
2253 assert_eq!(s.max_web_seeds, 4);
2254 assert_eq!(s.initial_picker_threshold, 4);
2255 assert_eq!(s.whole_pieces_threshold, 20);
2256 assert_eq!(s.snub_timeout_secs, 15);
2257 assert_eq!(s.readahead_pieces, 8);
2258 assert!(s.streaming_timeout_escalation);
2259 assert_eq!(s.max_peers_per_torrent, 128);
2260 assert_eq!(s.runtime_worker_threads, default_runtime_worker_threads());
2261 assert!(s.pin_cores);
2262 }
2263
2264 #[test]
2265 fn min_memory_preset() {
2266 let s = Settings::min_memory();
2267 assert_eq!(s.disk_cache_size, 8 * 1024 * 1024);
2268 assert_eq!(s.max_torrents, 20);
2269 assert_eq!(s.max_peers_per_torrent, 30);
2270 assert_eq!(s.active_downloads, 1);
2271 assert_eq!(s.active_seeds, 2);
2272 assert_eq!(s.active_limit, 10);
2273 assert_eq!(s.alert_channel_size, 256);
2274 assert_eq!(s.utp_max_connections, 64);
2275 assert_eq!(s.max_request_queue_depth, 50);
2276 assert_eq!(s.initial_queue_depth, 16);
2277 assert_eq!(s.max_concurrent_stream_reads, 2);
2278 assert_eq!(s.hashing_threads, 1);
2279 assert_eq!(s.disk_io_threads, 1);
2280 }
2281
2282 #[test]
2283 fn high_performance_preset() {
2284 let s = Settings::high_performance();
2285 assert_eq!(s.disk_cache_size, 256 * 1024 * 1024);
2286 assert_eq!(s.max_torrents, 2000);
2287 assert_eq!(s.max_peers_per_torrent, 200);
2288 assert_eq!(s.active_downloads, 30);
2289 assert_eq!(s.active_seeds, 100);
2290 assert_eq!(s.active_limit, 2000);
2291 assert_eq!(s.alert_channel_size, 4096);
2292 assert_eq!(s.utp_max_connections, 1024);
2293 assert_eq!(s.max_request_queue_depth, 1000);
2294 assert_eq!(s.initial_queue_depth, 256);
2295 assert_eq!(s.max_concurrent_stream_reads, 32);
2296 assert_eq!(s.hashing_threads, 4);
2297 assert_eq!(s.disk_io_threads, 8);
2298 assert_eq!(s.auto_upload_slots_max, 100);
2299 }
2300
2301 #[test]
2302 fn json_round_trip() {
2303 let original = Settings::default();
2304 let json = serde_json::to_string(&original).unwrap();
2305 let decoded: Settings = serde_json::from_str(&json).unwrap();
2306 assert_eq!(original, decoded);
2307 }
2308
2309 #[test]
2310 fn json_round_trip_presets() {
2311 for original in [Settings::min_memory(), Settings::high_performance()] {
2313 let json = serde_json::to_string(&original).unwrap();
2314 let decoded: Settings = serde_json::from_str(&json).unwrap();
2315 assert_eq!(original, decoded);
2316 }
2317 }
2318
2319 #[test]
2320 fn json_missing_fields_use_defaults() {
2321 let decoded: Settings = serde_json::from_str("{}").unwrap();
2323 assert_eq!(decoded, Settings::default());
2324 }
2325
2326 #[test]
2328 fn seed_time_limits_default_none() {
2329 let s = Settings::default();
2330 assert!(s.seed_time_limit_secs.is_none());
2331 assert!(s.inactive_seed_time_limit_secs.is_none());
2332 }
2333
2334 #[test]
2335 fn seed_time_limits_round_trip_json() {
2336 let s = Settings {
2337 seed_time_limit_secs: Some(3600),
2338 inactive_seed_time_limit_secs: Some(1800),
2339 ..Settings::default()
2340 };
2341 let json = serde_json::to_string(&s).unwrap();
2342 let decoded: Settings = serde_json::from_str(&json).unwrap();
2343 assert_eq!(decoded.seed_time_limit_secs, Some(3600));
2344 assert_eq!(decoded.inactive_seed_time_limit_secs, Some(1800));
2345 }
2346
2347 #[test]
2348 fn seed_time_limits_skipped_when_none() {
2349 let s = Settings::default();
2351 let json = serde_json::to_string(&s).unwrap();
2352 assert!(
2353 !json.contains("seed_time_limit_secs"),
2354 "None should not be serialised: {json}"
2355 );
2356 assert!(
2357 !json.contains("inactive_seed_time_limit_secs"),
2358 "None should not be serialised: {json}"
2359 );
2360 }
2361
2362 #[test]
2363 fn seed_time_limits_flow_to_torrent_config() {
2364 let s = Settings {
2365 seed_time_limit_secs: Some(7200),
2366 inactive_seed_time_limit_secs: Some(900),
2367 ..Settings::default()
2368 };
2369 let tc = crate::types::TorrentConfig::from(&s);
2370 assert_eq!(tc.seed_time_limit_secs, Some(7200));
2371 assert_eq!(tc.inactive_seed_time_limit_secs, Some(900));
2372 }
2373
2374 #[test]
2376 fn m171_settings_defaults_pause_true_false_false() {
2377 let s = Settings::default();
2378 assert_eq!(s.max_ratio_action, MaxRatioAction::Pause);
2379 assert!(
2380 s.create_subfolder,
2381 "create_subfolder defaults true (qBt factory default)"
2382 );
2383 assert!(!s.auto_manage_torrents);
2384 assert!(!s.queueing_enabled);
2385 }
2386
2387 #[test]
2388 fn m171_settings_round_trip_preserves_all_four() {
2389 let s = Settings {
2390 max_ratio_action: MaxRatioAction::EnableSuperSeeding,
2391 create_subfolder: false,
2392 auto_manage_torrents: true,
2393 queueing_enabled: true,
2394 ..Settings::default()
2395 };
2396 let json = serde_json::to_string(&s).unwrap();
2397 let decoded: Settings = serde_json::from_str(&json).unwrap();
2398 assert_eq!(decoded, s);
2399 }
2400
2401 #[test]
2402 fn max_ratio_action_wire_snake_case() {
2403 let pause = serde_json::to_string(&MaxRatioAction::Pause).unwrap();
2405 let remove = serde_json::to_string(&MaxRatioAction::Remove).unwrap();
2406 let super_seed = serde_json::to_string(&MaxRatioAction::EnableSuperSeeding).unwrap();
2407 assert_eq!(pause, "\"pause\"");
2408 assert_eq!(remove, "\"remove\"");
2409 assert_eq!(super_seed, "\"enable_super_seeding\"");
2410 }
2411
2412 #[test]
2413 fn max_ratio_action_wire_snake_case_round_trip() {
2414 let pause: MaxRatioAction = serde_json::from_str("\"pause\"").unwrap();
2416 let remove: MaxRatioAction = serde_json::from_str("\"remove\"").unwrap();
2417 let super_seed: MaxRatioAction = serde_json::from_str("\"enable_super_seeding\"").unwrap();
2418 assert_eq!(pause, MaxRatioAction::Pause);
2419 assert_eq!(remove, MaxRatioAction::Remove);
2420 assert_eq!(super_seed, MaxRatioAction::EnableSuperSeeding);
2421 }
2422
2423 #[test]
2424 fn validation_force_proxy_no_proxy() {
2425 let s = Settings {
2426 force_proxy: true,
2427 ..Settings::default()
2428 };
2429 let err = s.validate().unwrap_err();
2431 assert!(err.to_string().contains("force_proxy"));
2432 }
2433
2434 #[test]
2435 fn validation_valid_defaults() {
2436 Settings::default().validate().unwrap();
2437 Settings::min_memory().validate().unwrap();
2438 Settings::high_performance().validate().unwrap();
2439 }
2440
2441 #[test]
2442 fn disk_config_from_settings() {
2443 let s = Settings::default();
2444 let dc = crate::disk::DiskConfig::from(&s);
2445 assert_eq!(dc.io_threads, default_disk_io_threads());
2446 assert_eq!(dc.storage_mode, StorageMode::Auto);
2447 assert_eq!(dc.cache_size, 16 * 1024 * 1024);
2448 assert!((dc.write_cache_ratio - 0.5).abs() < f32::EPSILON);
2449 assert_eq!(dc.channel_capacity, 512);
2450 }
2451
2452 #[test]
2453 fn torrent_config_from_settings() {
2454 let s = Settings::default();
2455 let tc = crate::types::TorrentConfig::from(&s);
2456 assert_eq!(tc.listen_port, 0); assert_eq!(tc.max_peers, s.max_peers_per_torrent);
2458 assert_eq!(tc.download_dir, s.download_dir);
2459 assert_eq!(tc.enable_dht, s.enable_dht);
2460 assert_eq!(tc.enable_pex, s.enable_pex);
2461 assert_eq!(tc.encryption_mode, s.encryption_mode);
2462 assert_eq!(tc.enable_utp, s.enable_utp);
2463 assert_eq!(tc.enable_web_seed, s.enable_web_seed);
2464 assert_eq!(tc.hashing_threads, s.hashing_threads);
2465 assert_eq!(
2466 tc.max_concurrent_stream_reads,
2467 s.max_concurrent_stream_reads
2468 );
2469 assert_eq!(tc.anonymous_mode, s.anonymous_mode);
2470 assert_eq!(tc.enable_i2p, s.enable_i2p);
2471 assert_eq!(tc.allow_i2p_mixed, s.allow_i2p_mixed);
2472 assert_eq!(tc.strict_end_game, s.strict_end_game);
2474 assert_eq!(tc.upload_rate_limit, s.upload_rate_limit);
2475 assert_eq!(tc.download_rate_limit, s.download_rate_limit);
2476 assert_eq!(tc.max_web_seeds, s.max_web_seeds);
2477 assert_eq!(tc.initial_picker_threshold, s.initial_picker_threshold);
2478 assert_eq!(tc.whole_pieces_threshold, s.whole_pieces_threshold);
2479 assert_eq!(tc.snub_timeout_secs, s.snub_timeout_secs);
2480 assert_eq!(tc.readahead_pieces, s.readahead_pieces);
2481 assert_eq!(
2482 tc.streaming_timeout_escalation,
2483 s.streaming_timeout_escalation
2484 );
2485 assert_eq!(tc.storage_mode, s.storage_mode);
2487 assert_eq!(tc.block_request_timeout_secs, s.block_request_timeout_secs);
2488 assert_eq!(tc.enable_lsd, s.enable_lsd);
2489 assert_eq!(tc.force_proxy, s.force_proxy);
2490 assert_eq!(tc.steal_stale_piece_secs, 2);
2492 assert_eq!(tc.steal_stale_piece_secs, s.steal_stale_piece_secs);
2493 }
2494
2495 #[test]
2496 fn torrent_config_from_nondefault_settings() {
2497 let mut s = Settings {
2499 strict_end_game: false,
2500 upload_rate_limit: 1_000_000,
2501 download_rate_limit: 2_000_000,
2502 max_web_seeds: 8,
2503 initial_picker_threshold: 10,
2504 whole_pieces_threshold: 50,
2505 snub_timeout_secs: 120,
2506 readahead_pieces: 16,
2507 streaming_timeout_escalation: false,
2508 storage_mode: StorageMode::Full,
2509 block_request_timeout_secs: 30,
2510 enable_lsd: false,
2511 force_proxy: true,
2512 ..Settings::default()
2513 };
2514 s.proxy.proxy_type = crate::proxy::ProxyType::Socks5;
2515
2516 let tc = crate::types::TorrentConfig::from(&s);
2517 assert!(!tc.strict_end_game);
2518 assert_eq!(tc.upload_rate_limit, 1_000_000);
2519 assert_eq!(tc.download_rate_limit, 2_000_000);
2520 assert_eq!(tc.max_web_seeds, 8);
2521 assert_eq!(tc.initial_picker_threshold, 10);
2522 assert_eq!(tc.whole_pieces_threshold, 50);
2523 assert_eq!(tc.snub_timeout_secs, 120);
2524 assert_eq!(tc.readahead_pieces, 16);
2525 assert!(!tc.streaming_timeout_escalation);
2526 assert_eq!(tc.storage_mode, StorageMode::Full);
2527 assert_eq!(tc.block_request_timeout_secs, 30);
2528 assert!(!tc.enable_lsd);
2529 assert!(tc.force_proxy);
2530 }
2531
2532 #[test]
2533 fn external_ip_default_and_json() {
2534 let s = Settings::default();
2535 assert!(s.external_ip.is_none());
2536
2537 let json = r#"{"external_ip": "203.0.113.5"}"#;
2539 let decoded: Settings = serde_json::from_str(json).unwrap();
2540 assert_eq!(
2541 decoded.external_ip,
2542 Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(
2543 203, 0, 113, 5
2544 )))
2545 );
2546
2547 let encoded = serde_json::to_string(&decoded).unwrap();
2549 let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
2550 assert_eq!(roundtrip.external_ip, decoded.external_ip);
2551 }
2552
2553 #[test]
2554 fn validation_zero_threads() {
2555 let s = Settings {
2556 hashing_threads: 0,
2557 ..Settings::default()
2558 };
2559 let err = s.validate().unwrap_err();
2560 assert!(err.to_string().contains("hashing_threads"));
2561
2562 let s = Settings {
2563 disk_io_threads: 0,
2564 ..Settings::default()
2565 };
2566 let err = s.validate().unwrap_err();
2567 assert!(err.to_string().contains("disk_io_threads"));
2568
2569 let s = Settings {
2570 max_blocking_threads: 0,
2571 ..Settings::default()
2572 };
2573 let err = s.validate().unwrap_err();
2574 assert!(err.to_string().contains("max_blocking_threads"));
2575 }
2576
2577 #[test]
2578 fn share_mode_requires_fast_extension() {
2579 let mut s = Settings {
2580 default_share_mode: true,
2581 enable_fast_extension: false,
2582 ..Settings::default()
2583 };
2584 let err = s.validate().unwrap_err();
2585 assert!(err.to_string().contains("share_mode"));
2586
2587 s.enable_fast_extension = true;
2589 s.validate().unwrap();
2590 }
2591
2592 #[test]
2593 fn share_mode_default_false() {
2594 let cfg = crate::types::TorrentConfig::default();
2595 assert!(!cfg.share_mode);
2596 }
2597
2598 #[test]
2599 fn dht_storage_settings_defaults() {
2600 let s = Settings::default();
2601 assert_eq!(s.dht_max_items, 700);
2602 assert_eq!(s.dht_item_lifetime_secs, 7200);
2603 }
2604
2605 #[test]
2606 fn dht_sample_interval_default_disabled() {
2607 let s = Settings::default();
2608 assert_eq!(s.dht_sample_infohashes_interval, 0);
2609 }
2610
2611 #[test]
2612 fn dht_sample_interval_json_round_trip() {
2613 let json = r#"{"dht_sample_infohashes_interval": 300}"#;
2614 let decoded: Settings = serde_json::from_str(json).unwrap();
2615 assert_eq!(decoded.dht_sample_infohashes_interval, 300);
2616
2617 let encoded = serde_json::to_string(&decoded).unwrap();
2618 let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
2619 assert_eq!(roundtrip.dht_sample_infohashes_interval, 300);
2620 }
2621
2622 #[test]
2623 fn min_memory_restricts_dht_items() {
2624 let s = Settings::min_memory();
2625 assert_eq!(s.dht_max_items, 100);
2626 }
2627
2628 #[test]
2629 fn dht_config_inherits_security_settings() {
2630 let s = Settings {
2631 dht_enforce_node_id: false,
2632 ..Settings::default()
2633 };
2634 let dht = s.to_dht_config();
2635 assert!(!dht.enforce_node_id);
2636 assert!(dht.restrict_routing_ips);
2637
2638 let dht_v6 = s.to_dht_config_v6();
2639 assert!(!dht_v6.enforce_node_id);
2640 assert!(dht_v6.restrict_routing_ips);
2641 }
2642
2643 #[test]
2644 fn enable_holepunch_default_true() {
2645 let s = Settings::default();
2646 assert!(s.enable_holepunch);
2647 }
2648
2649 #[test]
2650 fn enable_holepunch_json_round_trip() {
2651 let json = r#"{"enable_holepunch": false}"#;
2652 let decoded: Settings = serde_json::from_str(json).unwrap();
2653 assert!(!decoded.enable_holepunch);
2654
2655 let encoded = serde_json::to_string(&decoded).unwrap();
2656 let roundtrip: Settings = serde_json::from_str(&encoded).unwrap();
2657 assert!(!roundtrip.enable_holepunch);
2658 }
2659
2660 #[test]
2661 fn i2p_settings_defaults() {
2662 let s = Settings::default();
2663 assert!(!s.enable_i2p);
2664 assert_eq!(s.i2p_hostname, "127.0.0.1");
2665 assert_eq!(s.i2p_port, 7656);
2666 assert_eq!(s.i2p_inbound_quantity, 3);
2667 assert_eq!(s.i2p_outbound_quantity, 3);
2668 assert_eq!(s.i2p_inbound_length, 3);
2669 assert_eq!(s.i2p_outbound_length, 3);
2670 assert!(!s.allow_i2p_mixed);
2671 }
2672
2673 #[test]
2674 fn i2p_settings_json_roundtrip() {
2675 let s = Settings {
2676 enable_i2p: true,
2677 i2p_hostname: "10.0.0.1".into(),
2678 i2p_port: 7700,
2679 i2p_inbound_quantity: 5,
2680 i2p_outbound_quantity: 4,
2681 i2p_inbound_length: 2,
2682 i2p_outbound_length: 1,
2683 allow_i2p_mixed: true,
2684 ..Settings::default()
2685 };
2686 let json = serde_json::to_string(&s).unwrap();
2687 let decoded: Settings = serde_json::from_str(&json).unwrap();
2688 assert_eq!(s, decoded);
2689 }
2690
2691 #[test]
2692 fn i2p_validation_quantity_zero() {
2693 let s = Settings {
2694 enable_i2p: true,
2695 i2p_inbound_quantity: 0,
2696 ..Settings::default()
2697 };
2698 let err = s.validate().unwrap_err();
2699 assert!(err.to_string().contains("i2p_inbound_quantity"));
2700 }
2701
2702 #[test]
2703 fn i2p_validation_quantity_too_high() {
2704 let s = Settings {
2705 enable_i2p: true,
2706 i2p_outbound_quantity: 17,
2707 ..Settings::default()
2708 };
2709 let err = s.validate().unwrap_err();
2710 assert!(err.to_string().contains("i2p_outbound_quantity"));
2711 }
2712
2713 #[test]
2714 fn i2p_validation_length_too_high() {
2715 let s = Settings {
2716 enable_i2p: true,
2717 i2p_inbound_length: 8,
2718 ..Settings::default()
2719 };
2720 let err = s.validate().unwrap_err();
2721 assert!(err.to_string().contains("i2p_inbound_length"));
2722 }
2723
2724 #[test]
2725 fn i2p_validation_passes_when_disabled() {
2726 let mut s = Settings {
2728 enable_i2p: false,
2729 ..Settings::default()
2730 };
2731 s.i2p_inbound_quantity = 0; s.validate().unwrap(); }
2734
2735 #[test]
2736 fn i2p_validation_valid_config() {
2737 let s = Settings {
2738 enable_i2p: true,
2739 i2p_inbound_quantity: 1,
2740 i2p_outbound_quantity: 16,
2741 i2p_inbound_length: 0,
2742 i2p_outbound_length: 7,
2743 ..Settings::default()
2744 };
2745 s.validate().unwrap();
2746 }
2747
2748 #[test]
2749 fn ssl_settings_defaults() {
2750 let s = Settings::default();
2751 assert_eq!(s.ssl_listen_port, 0);
2752 assert!(s.ssl_cert_path.is_none());
2753 assert!(s.ssl_key_path.is_none());
2754 }
2755
2756 #[test]
2757 fn ssl_settings_json_round_trip() {
2758 let s = Settings {
2759 ssl_listen_port: 4433,
2760 ssl_cert_path: Some(PathBuf::from("/etc/ssl/cert.pem")),
2761 ssl_key_path: Some(PathBuf::from("/etc/ssl/key.pem")),
2762 ..Settings::default()
2763 };
2764 let json = serde_json::to_string(&s).unwrap();
2765 let decoded: Settings = serde_json::from_str(&json).unwrap();
2766 assert_eq!(s, decoded);
2767 }
2768
2769 #[test]
2770 fn ssl_validation_cert_without_key() {
2771 let s = Settings {
2772 ssl_cert_path: Some(PathBuf::from("/tmp/cert.pem")),
2773 ..Settings::default()
2774 };
2775 let err = s.validate().unwrap_err();
2777 assert!(err.to_string().contains("ssl_cert_path"));
2778 }
2779
2780 #[test]
2781 fn ssl_validation_key_without_cert() {
2782 let s = Settings {
2783 ssl_key_path: Some(PathBuf::from("/tmp/key.pem")),
2784 ..Settings::default()
2785 };
2786 let err = s.validate().unwrap_err();
2788 assert!(err.to_string().contains("ssl_cert_path"));
2789 }
2790
2791 #[test]
2792 fn ssl_validation_both_set_passes() {
2793 let s = Settings {
2794 ssl_cert_path: Some(PathBuf::from("/tmp/cert.pem")),
2795 ssl_key_path: Some(PathBuf::from("/tmp/key.pem")),
2796 ..Settings::default()
2797 };
2798 s.validate().unwrap();
2799 }
2800
2801 #[test]
2802 fn ssl_validation_both_absent_passes() {
2803 let s = Settings::default();
2804 s.validate().unwrap();
2806 }
2807
2808 #[test]
2809 fn default_choking_algorithms() {
2810 let s = Settings::default();
2811 assert_eq!(
2812 s.seed_choking_algorithm,
2813 SeedChokingAlgorithm::FastestUpload
2814 );
2815 assert_eq!(s.choking_algorithm, ChokingAlgorithm::FixedSlots);
2816 }
2817
2818 #[test]
2819 fn choking_algorithm_json_round_trip() {
2820 let s = Settings {
2821 seed_choking_algorithm: SeedChokingAlgorithm::AntiLeech,
2822 choking_algorithm: ChokingAlgorithm::RateBased,
2823 ..Settings::default()
2824 };
2825 let json = serde_json::to_string(&s).unwrap();
2826 let decoded: Settings = serde_json::from_str(&json).unwrap();
2827 assert_eq!(
2828 decoded.seed_choking_algorithm,
2829 SeedChokingAlgorithm::AntiLeech
2830 );
2831 assert_eq!(decoded.choking_algorithm, ChokingAlgorithm::RateBased);
2832 }
2833
2834 #[test]
2835 fn m44_settings_defaults() {
2836 let s = Settings::default();
2837 assert!(s.piece_extent_affinity);
2838 assert!(s.suggest_mode);
2839 assert_eq!(s.max_suggest_pieces, 16);
2840 assert_eq!(s.predictive_piece_announce_ms, 0);
2841 }
2842
2843 #[test]
2844 fn m44_high_performance_enables_suggest() {
2845 let s = Settings::high_performance();
2846 assert!(s.suggest_mode);
2847 }
2848
2849 #[test]
2850 fn m44_json_round_trip() {
2851 let s = Settings {
2852 piece_extent_affinity: false,
2853 suggest_mode: true,
2854 max_suggest_pieces: 5,
2855 predictive_piece_announce_ms: 50,
2856 ..Settings::default()
2857 };
2858 let json = serde_json::to_string(&s).unwrap();
2859 let decoded: Settings = serde_json::from_str(&json).unwrap();
2860 assert_eq!(s, decoded);
2861 }
2862
2863 #[test]
2864 fn security_settings_defaults() {
2865 let s = Settings::default();
2866 assert!(s.ssrf_mitigation);
2867 assert!(!s.allow_idna);
2868 assert!(s.validate_https_trackers);
2869 }
2870
2871 #[test]
2872 fn security_settings_json_round_trip() {
2873 let s = Settings {
2874 ssrf_mitigation: false,
2875 allow_idna: true,
2876 validate_https_trackers: false,
2877 ..Settings::default()
2878 };
2879 let json = serde_json::to_string(&s).unwrap();
2880 let decoded: Settings = serde_json::from_str(&json).unwrap();
2881 assert_eq!(s, decoded);
2882 }
2883
2884 #[test]
2885 fn security_settings_missing_use_defaults() {
2886 let decoded: Settings = serde_json::from_str("{}").unwrap();
2888 assert!(decoded.ssrf_mitigation);
2889 assert!(!decoded.allow_idna);
2890 assert!(decoded.validate_https_trackers);
2891 }
2892
2893 #[test]
2894 fn url_security_config_from_settings() {
2895 let s = Settings {
2896 ssrf_mitigation: false,
2897 allow_idna: true,
2898 validate_https_trackers: false,
2899 ..Settings::default()
2900 };
2901 let cfg = crate::url_guard::UrlSecurityConfig::from(&s);
2902 assert!(!cfg.ssrf_mitigation);
2903 assert!(cfg.allow_idna);
2904 assert!(!cfg.validate_https_trackers);
2905 }
2906
2907 #[test]
2908 fn default_peer_dscp_value() {
2909 let s = Settings::default();
2910 assert_eq!(s.peer_dscp, 0x08);
2911 }
2912
2913 #[test]
2914 fn peer_dscp_json_round_trip() {
2915 let s = Settings {
2916 peer_dscp: 0x2E, ..Settings::default()
2918 };
2919 let json = serde_json::to_string(&s).unwrap();
2920 let decoded: Settings = serde_json::from_str(&json).unwrap();
2921 assert_eq!(decoded.peer_dscp, 0x2E);
2922 }
2923
2924 #[test]
2925 fn peer_dscp_zero_disables() {
2926 let s = Settings {
2927 peer_dscp: 0,
2928 ..Settings::default()
2929 };
2930 let json = serde_json::to_string(&s).unwrap();
2931 let decoded: Settings = serde_json::from_str(&json).unwrap();
2932 assert_eq!(decoded.peer_dscp, 0);
2933 }
2934
2935 #[test]
2936 fn utp_config_includes_dscp() {
2937 let s = Settings {
2938 peer_dscp: 0x0A,
2939 ..Settings::default()
2940 };
2941 let utp = s.to_utp_config(6881);
2942 assert_eq!(utp.dscp, 0x0A);
2943
2944 let utp_v6 = s.to_utp_config_v6(6881);
2945 assert_eq!(utp_v6.dscp, 0x0A);
2946 }
2947
2948 #[test]
2949 fn default_stats_report_interval() {
2950 let s = Settings::default();
2951 assert_eq!(s.stats_report_interval, 1000);
2952 }
2953
2954 #[test]
2955 fn stats_report_interval_json_round_trip() {
2956 let s = Settings {
2957 stats_report_interval: 5000,
2958 ..Settings::default()
2959 };
2960 let json = serde_json::to_string(&s).unwrap();
2961 let decoded: Settings = serde_json::from_str(&json).unwrap();
2962 assert_eq!(decoded.stats_report_interval, 5000);
2963 }
2964
2965 #[test]
2966 fn stats_report_interval_zero_disables() {
2967 let s = Settings {
2968 stats_report_interval: 0,
2969 ..Settings::default()
2970 };
2971 let json = serde_json::to_string(&s).unwrap();
2972 let decoded: Settings = serde_json::from_str(&json).unwrap();
2973 assert_eq!(decoded.stats_report_interval, 0);
2974 }
2975
2976 #[test]
2977 fn settings_runtime_worker_threads_and_pin_cores() {
2978 let s = Settings::default();
2980 assert_eq!(s.runtime_worker_threads, default_runtime_worker_threads());
2981 assert!(s.pin_cores);
2982
2983 let mut s = Settings {
2985 runtime_worker_threads: 0,
2986 ..Settings::default()
2987 };
2988 assert!(s.validate().is_ok());
2989
2990 s.runtime_worker_threads = 256;
2992 assert!(s.validate().is_ok());
2993
2994 s.runtime_worker_threads = 257;
2996 assert!(s.validate().is_err());
2997 }
2998
2999 #[test]
3000 fn max_in_flight_512_default() {
3001 let s = Settings::default();
3002 assert_eq!(s.max_in_flight_pieces, 512);
3003 assert_eq!(s.fixed_pipeline_depth, 128);
3004
3005 let mm = Settings::min_memory();
3007 assert_eq!(mm.max_in_flight_pieces, 32);
3008 assert_eq!(mm.fixed_pipeline_depth, 32);
3009
3010 let hp = Settings::high_performance();
3011 assert_eq!(hp.max_in_flight_pieces, 512);
3012 assert_eq!(hp.fixed_pipeline_depth, 128); }
3014
3015 #[test]
3016 fn recalc_max_in_flight_formula() {
3017 let base = 512_usize;
3020
3021 let connected = 10;
3023 let num_pieces = 2000_u32;
3024 let calculated = base.max(connected * 4);
3025 let result = calculated.min(num_pieces as usize / 2).max(base);
3026 assert_eq!(result, 512); let connected = 200;
3030 let calculated = base.max(connected * 4);
3031 let result = calculated.min(num_pieces as usize / 2).max(base);
3032 assert_eq!(result, 800); let connected = 200;
3036 let num_pieces = 100_u32;
3037 let calculated = base.max(connected * 4);
3038 let result = calculated.min(num_pieces as usize / 2).max(base);
3039 assert_eq!(result, 512); let connected = 129; let num_pieces = 10000_u32;
3044 let calculated = base.max(connected * 4);
3045 let result = calculated.min(num_pieces as usize / 2).max(base);
3046 assert_eq!(result, 516); }
3048
3049 #[test]
3052 fn settings_default_enables_qbt_compat_v0_172_1() {
3053 let s = Settings::default();
3058 assert!(s.qbt_compat.enabled);
3059 assert_eq!(s.qbt_compat.username, "admin");
3060 assert_eq!(s.qbt_compat.password, "");
3064 assert!(
3065 s.qbt_compat
3066 .password_hash
3067 .starts_with("$argon2id$v=19$m=19456,t=2,p=1$")
3068 );
3069 assert_eq!(s.qbt_compat.spoof_app_version, "v5.1.4");
3070 assert_eq!(s.qbt_compat.spoof_webapi_version, "2.11.4");
3071 assert_eq!(s.qbt_compat.session_ttl_secs, 86_400);
3072 assert_eq!(s.qbt_compat.max_sessions, 1024);
3073 assert!(s.qbt_compat.max_concurrent_argon2_ops.is_none());
3074 }
3075
3076 #[test]
3077 fn validate_rejects_empty_username() {
3078 let mut s = Settings::default();
3079 s.qbt_compat.enabled = true;
3080 s.qbt_compat.username = String::new();
3081 let err = s.validate().expect_err("empty username must fail");
3082 let msg = format!("{err}");
3083 assert!(msg.contains("username"), "error was: {msg}");
3084 }
3085
3086 #[test]
3087 fn validate_rejects_short_legacy_password_lt_8_when_hash_empty() {
3088 let mut s = Settings::default();
3089 s.qbt_compat.enabled = true;
3090 s.qbt_compat.password_hash.clear();
3092 s.qbt_compat.password = "short".into();
3093 let err = s.validate().expect_err("short password must fail");
3094 let msg = format!("{err}");
3095 assert!(
3096 msg.contains("password") && msg.contains("hash"),
3097 "error was: {msg}"
3098 );
3099 }
3100
3101 #[test]
3102 fn validate_rejects_bad_app_version_format() {
3103 let mut s = Settings::default();
3104 s.qbt_compat.enabled = true;
3105 s.qbt_compat.spoof_app_version = "garbage".into();
3106 let err = s.validate().expect_err("bad app version must fail");
3107 let msg = format!("{err}");
3108 assert!(msg.contains("spoof_app_version"), "error was: {msg}");
3109 }
3110
3111 #[test]
3112 fn validate_rejects_bad_webapi_version_format() {
3113 let mut s = Settings::default();
3114 s.qbt_compat.enabled = true;
3115 s.qbt_compat.spoof_webapi_version = "v2.11".into(); let err = s.validate().expect_err("bad webapi version must fail");
3117 let msg = format!("{err}");
3118 assert!(msg.contains("spoof_webapi_version"), "error was: {msg}");
3119 }
3120
3121 #[test]
3122 fn validate_rejects_ttl_out_of_bounds() {
3123 let mut s = Settings::default();
3124 s.qbt_compat.enabled = true;
3125 s.qbt_compat.session_ttl_secs = 10; let err = s.validate().expect_err("ttl too small must fail");
3127 assert!(format!("{err}").contains("session_ttl_secs"));
3128
3129 let mut s = Settings::default();
3130 s.qbt_compat.enabled = true;
3131 s.qbt_compat.session_ttl_secs = 604_801; let err = s.validate().expect_err("ttl too large must fail");
3133 assert!(format!("{err}").contains("session_ttl_secs"));
3134 }
3135
3136 #[test]
3139 fn default_hash_roundtrips_admin_admin() {
3140 use argon2::Argon2;
3141 use argon2::password_hash::{PasswordHash, PasswordVerifier};
3142
3143 let hash = PasswordHash::new(DEFAULT_ADMINADMIN_HASH)
3153 .expect("DEFAULT_ADMINADMIN_HASH must be a valid PHC string");
3154 Argon2::default()
3155 .verify_password(b"adminadmin", &hash)
3156 .expect("default hash must verify the 'adminadmin' plaintext");
3157 }
3158
3159 #[test]
3160 fn validate_rejects_password_hash_not_starting_with_argon2id() {
3161 let mut s = Settings::default();
3162 s.qbt_compat.enabled = true;
3163 s.qbt_compat.password_hash =
3165 "$2b$12$KIXQ5.pHJN3iLz9H6CfQEe2/6rFv1h4jdXWv.0eoGzJ6w7L4Yj7vi".into();
3166 let err = s.validate().expect_err("non-argon2id hash must fail");
3167 let msg = format!("{err}");
3168 assert!(msg.contains("argon2id"), "error was: {msg}");
3169 }
3170
3171 #[test]
3172 fn validate_rejects_zero_max_concurrent_argon2_ops() {
3173 let mut s = Settings::default();
3174 s.qbt_compat.enabled = true;
3175 s.qbt_compat.max_concurrent_argon2_ops = Some(0);
3176 let err = s.validate().expect_err("zero argon2 semaphore must fail");
3177 assert!(format!("{err}").contains("max_concurrent_argon2_ops"));
3178 }
3179
3180 #[test]
3181 fn default_settings_ship_pre_hashed_no_migration_needed() {
3182 let s = Settings::default();
3183 assert!(s.qbt_compat.password_hash.starts_with("$argon2id$"));
3184 assert!(s.qbt_compat.password.is_empty());
3185 }
3186
3187 #[test]
3188 fn hash_qbt_password_roundtrips() {
3189 let h = hash_qbt_password("correct horse battery staple")
3190 .expect("hash must succeed for a simple plaintext");
3191 assert!(h.starts_with("$argon2id$v=19$m=19456,t=2,p=1$"));
3192 let h2 =
3194 hash_qbt_password("correct horse battery staple").expect("second hash must succeed");
3195 assert_ne!(h, h2, "argon2 must use a fresh salt per call");
3196 }
3197
3198 #[test]
3199 fn migrate_qbt_credentials_noop_when_hash_present() {
3200 let mut qbt = QbtCompatSettings {
3201 password_hash: DEFAULT_ADMINADMIN_HASH.into(),
3202 password: String::new(),
3203 ..Default::default()
3204 };
3205 let outcome = migrate_qbt_credentials(&mut qbt).expect("noop");
3206 assert_eq!(outcome, QbtCredentialMigration::NoOp);
3207 assert_eq!(qbt.password_hash, DEFAULT_ADMINADMIN_HASH);
3208 assert!(qbt.password.is_empty());
3209 }
3210
3211 #[test]
3212 fn migrate_qbt_credentials_upgrades_legacy_plaintext() {
3213 use argon2::Argon2;
3214 use argon2::password_hash::{PasswordHash, PasswordVerifier};
3215
3216 let mut qbt = QbtCompatSettings {
3217 password_hash: String::new(),
3218 password: "legacy-plaintext-pw".into(),
3219 ..Default::default()
3220 };
3221 let outcome = migrate_qbt_credentials(&mut qbt).expect("upgrade");
3222 assert_eq!(outcome, QbtCredentialMigration::Upgraded);
3223 assert!(qbt.password_hash.starts_with("$argon2id$"));
3224 assert!(
3225 qbt.password.is_empty(),
3226 "plaintext must be zeroed after migration"
3227 );
3228
3229 let parsed =
3230 PasswordHash::new(&qbt.password_hash).expect("migration wrote a valid PHC string");
3231 Argon2::default()
3232 .verify_password(b"legacy-plaintext-pw", &parsed)
3233 .expect("migrated hash must verify the original plaintext");
3234 }
3235
3236 #[test]
3237 fn migrate_qbt_credentials_noop_when_both_empty() {
3238 let mut qbt = QbtCompatSettings {
3239 password_hash: String::new(),
3240 password: String::new(),
3241 ..Default::default()
3242 };
3243 let outcome = migrate_qbt_credentials(&mut qbt).expect("noop on empty");
3244 assert_eq!(outcome, QbtCredentialMigration::NoOp);
3245 }
3246
3247 #[test]
3250 fn brute_force_defaults_are_5_attempts_and_one_hour_ban() {
3251 let s = Settings::default();
3252 assert_eq!(s.qbt_compat.max_failed_auth_count, 5);
3253 assert_eq!(s.qbt_compat.ban_duration_secs, 3_600);
3254 assert!(!s.qbt_compat.bypass_local_auth);
3255 assert!(s.qbt_compat.bypass_auth_subnet_whitelist.is_empty());
3256 assert!(s.qbt_compat.brute_force_registry_capacity.is_none());
3257 }
3258
3259 #[test]
3260 fn validate_rejects_zero_max_failed_auth_count_without_bypass() {
3261 let mut s = Settings::default();
3262 s.qbt_compat.enabled = true;
3263 s.qbt_compat.max_failed_auth_count = 0;
3264 s.qbt_compat.bypass_local_auth = false;
3265 let err = s
3266 .validate()
3267 .expect_err("zero attempts without bypass must fail");
3268 assert!(format!("{err}").contains("max_failed_auth_count"));
3269 }
3270
3271 #[test]
3272 fn validate_accepts_zero_max_failed_auth_count_when_bypass_local() {
3273 let mut s = Settings::default();
3274 s.qbt_compat.enabled = true;
3275 s.qbt_compat.max_failed_auth_count = 0;
3276 s.qbt_compat.bypass_local_auth = true;
3277 s.validate().expect("bypass_local_auth disarms the check");
3278 }
3279
3280 #[test]
3281 fn validate_rejects_ban_duration_out_of_bounds() {
3282 let mut s = Settings::default();
3283 s.qbt_compat.enabled = true;
3284 s.qbt_compat.ban_duration_secs = 59;
3285 let err = s.validate().expect_err("too short ban must fail");
3286 assert!(format!("{err}").contains("ban_duration_secs"));
3287
3288 let mut s = Settings::default();
3289 s.qbt_compat.enabled = true;
3290 s.qbt_compat.ban_duration_secs = 86_401;
3291 let err = s.validate().expect_err("too long ban must fail");
3292 assert!(format!("{err}").contains("ban_duration_secs"));
3293 }
3294
3295 #[test]
3296 fn validate_rejects_malformed_bypass_whitelist_cidr() {
3297 let mut s = Settings::default();
3298 s.qbt_compat.enabled = true;
3299 s.qbt_compat.bypass_auth_subnet_whitelist = vec!["not-a-cidr".into()];
3300 let err = s.validate().expect_err("bad cidr must fail");
3301 let msg = format!("{err}");
3302 assert!(msg.contains("bypass_auth_subnet_whitelist"));
3303 assert!(msg.contains("not-a-cidr"));
3304 }
3305
3306 #[test]
3307 fn validate_accepts_valid_bypass_whitelist_cidrs() {
3308 let mut s = Settings::default();
3309 s.qbt_compat.enabled = true;
3310 s.qbt_compat.bypass_auth_subnet_whitelist = vec![
3311 "10.0.0.0/8".into(),
3312 "192.168.1.0/24".into(),
3313 "::1/128".into(),
3314 ];
3315 s.validate().expect("valid cidrs pass");
3316 }
3317
3318 #[test]
3319 fn validate_rejects_registry_capacity_below_floor() {
3320 let mut s = Settings::default();
3321 s.qbt_compat.enabled = true;
3322 s.qbt_compat.brute_force_registry_capacity = Some(99);
3323 let err = s
3324 .validate()
3325 .expect_err("capacity < 100 must fail sanity floor");
3326 assert!(format!("{err}").contains("brute_force_registry_capacity"));
3327 }
3328
3329 #[test]
3332 fn validate_rejects_zero_max_uploads_per_torrent() {
3333 let s = Settings {
3334 max_uploads_per_torrent: 0,
3335 ..Settings::default()
3336 };
3337 let err = s
3338 .validate()
3339 .expect_err("max_uploads_per_torrent = 0 must fail");
3340 let msg = format!("{err}");
3341 assert!(
3342 msg.contains("max_uploads_per_torrent"),
3343 "error was: {msg}"
3344 );
3345 }
3346
3347 #[test]
3348 fn validate_rejects_negative_below_minus_one_max_uploads_per_torrent() {
3349 let s = Settings {
3350 max_uploads_per_torrent: -2,
3351 ..Settings::default()
3352 };
3353 let err = s
3354 .validate()
3355 .expect_err("max_uploads_per_torrent < -1 must fail");
3356 let msg = format!("{err}");
3357 assert!(
3358 msg.contains("max_uploads_per_torrent"),
3359 "error was: {msg}"
3360 );
3361 }
3362
3363 #[test]
3364 fn validate_accepts_minus_one_max_uploads_per_torrent() {
3365 let s = Settings::default();
3366 assert_eq!(s.max_uploads_per_torrent, -1);
3367 s.validate().expect("default -1 must validate");
3368 }
3369
3370 #[test]
3371 fn validate_accepts_positive_max_uploads_per_torrent() {
3372 let s = Settings {
3373 max_uploads_per_torrent: 4,
3374 ..Settings::default()
3375 };
3376 s.validate().expect("n >= 1 must validate");
3377 }
3378
3379 #[test]
3380 fn max_uploads_per_torrent_default_deserialize_without_field() {
3381 let s = Settings::default();
3385 let mut value = serde_json::to_value(&s).expect("serialise");
3386 let obj = value.as_object_mut().expect("Settings is a JSON object");
3387 assert!(
3388 obj.remove("max_uploads_per_torrent").is_some(),
3389 "field should have been present in serialised default"
3390 );
3391 let decoded: Settings = serde_json::from_value(value).expect("deserialise without field");
3392 assert_eq!(decoded.max_uploads_per_torrent, -1);
3393 decoded.validate().expect("default-via-serde must validate");
3394 }
3395
3396 #[test]
3397 fn brute_force_settings_json_round_trip() {
3398 let mut s = Settings::default();
3399 s.qbt_compat.max_failed_auth_count = 7;
3400 s.qbt_compat.ban_duration_secs = 1_800;
3401 s.qbt_compat.bypass_local_auth = true;
3402 s.qbt_compat.bypass_auth_subnet_whitelist = vec!["10.0.0.0/8".into()];
3403 s.qbt_compat.brute_force_registry_capacity = Some(5_000);
3404
3405 let json = serde_json::to_string(&s).expect("serialise");
3406 let decoded: Settings = serde_json::from_str(&json).expect("deserialise");
3407 assert_eq!(decoded.qbt_compat.max_failed_auth_count, 7);
3408 assert_eq!(decoded.qbt_compat.ban_duration_secs, 1_800);
3409 assert!(decoded.qbt_compat.bypass_local_auth);
3410 assert_eq!(
3411 decoded.qbt_compat.bypass_auth_subnet_whitelist,
3412 vec!["10.0.0.0/8".to_string()]
3413 );
3414 assert_eq!(
3415 decoded.qbt_compat.brute_force_registry_capacity,
3416 Some(5_000)
3417 );
3418 }
3419
3420 #[test]
3423 fn settings_default_notify_on_complete_is_false() {
3424 assert!(!Settings::default().notify_on_complete);
3425 }
3426
3427 #[test]
3428 fn settings_default_notify_on_error_is_false() {
3429 assert!(!Settings::default().notify_on_error);
3430 }
3431
3432 #[test]
3433 fn settings_default_on_complete_program_is_none() {
3434 assert!(Settings::default().on_complete_program.is_none());
3435 }
3436
3437 #[test]
3438 fn settings_default_use_incomplete_dir_is_false() {
3439 assert!(!Settings::default().use_incomplete_dir);
3440 }
3441
3442 #[test]
3443 fn settings_default_incomplete_dir_is_none() {
3444 assert!(Settings::default().incomplete_dir.is_none());
3445 }
3446
3447 #[test]
3448 fn settings_default_default_skip_hash_check_is_false() {
3449 assert!(!Settings::default().default_skip_hash_check);
3450 }
3451
3452 #[test]
3453 fn settings_default_incomplete_extension_enabled_is_true() {
3454 assert!(Settings::default().incomplete_extension_enabled);
3455 }
3456
3457 #[test]
3458 fn settings_default_watched_folder_is_none() {
3459 assert!(Settings::default().watched_folder.is_none());
3460 }
3461
3462 #[test]
3463 fn settings_default_delete_torrent_after_add_is_false() {
3464 assert!(!Settings::default().delete_torrent_after_add);
3465 }
3466
3467 #[test]
3468 fn settings_default_move_completed_enabled_is_false() {
3469 assert!(!Settings::default().move_completed_enabled);
3470 }
3471
3472 #[test]
3473 fn settings_default_move_completed_to_is_none() {
3474 assert!(Settings::default().move_completed_to.is_none());
3475 }
3476
3477 #[test]
3478 fn settings_default_web_ui_https_enabled_is_false() {
3479 assert!(!Settings::default().web_ui_https_enabled);
3480 }
3481
3482 #[test]
3483 fn settings_default_network_interface_is_none() {
3484 assert!(Settings::default().network_interface.is_none());
3485 }
3486
3487 #[test]
3488 fn settings_default_default_add_paused_is_false() {
3489 assert!(!Settings::default().default_add_paused);
3490 }
3491
3492 #[test]
3495 fn validate_rejects_use_incomplete_dir_without_incomplete_dir() {
3496 let s = Settings {
3497 use_incomplete_dir: true,
3498 incomplete_dir: None,
3499 ..Settings::default()
3500 };
3501 let err = s.validate().expect_err("must require incomplete_dir");
3502 assert!(format!("{err}").contains("incomplete_dir"));
3503 }
3504
3505 #[test]
3506 fn validate_accepts_use_incomplete_dir_with_incomplete_dir() {
3507 let s = Settings {
3508 use_incomplete_dir: true,
3509 incomplete_dir: Some(PathBuf::from("/tmp/irontide-incomplete")),
3510 ..Settings::default()
3511 };
3512 s.validate().expect("paired fields valid");
3513 }
3514
3515 #[test]
3516 fn validate_rejects_move_completed_without_move_completed_to() {
3517 let s = Settings {
3518 move_completed_enabled: true,
3519 move_completed_to: None,
3520 ..Settings::default()
3521 };
3522 let err = s.validate().expect_err("must require move_completed_to");
3523 assert!(format!("{err}").contains("move_completed_to"));
3524 }
3525
3526 #[test]
3527 fn validate_rejects_relative_watched_folder() {
3528 let s = Settings {
3529 watched_folder: Some(PathBuf::from("relative/path")),
3530 ..Settings::default()
3531 };
3532 let err = s.validate().expect_err("relative path must fail");
3533 assert!(format!("{err}").contains("absolute"));
3534 }
3535
3536 #[test]
3537 fn validate_rejects_relative_incomplete_dir() {
3538 let s = Settings {
3539 incomplete_dir: Some(PathBuf::from("inc")),
3540 ..Settings::default()
3541 };
3542 let err = s.validate().expect_err("relative path must fail");
3543 assert!(format!("{err}").contains("absolute"));
3544 }
3545
3546 #[test]
3547 fn validate_rejects_relative_move_completed_to() {
3548 let s = Settings {
3549 move_completed_to: Some(PathBuf::from("done")),
3550 ..Settings::default()
3551 };
3552 let err = s.validate().expect_err("relative path must fail");
3553 assert!(format!("{err}").contains("absolute"));
3554 }
3555
3556 #[test]
3557 fn validate_rejects_system_path_as_watched_folder() {
3558 for sys in ["/", "/etc", "/usr", "/bin", "/sys", "/proc"] {
3559 let s = Settings {
3560 watched_folder: Some(PathBuf::from(sys)),
3561 ..Settings::default()
3562 };
3563 let err = s
3564 .validate()
3565 .expect_err("system path must be rejected");
3566 assert!(
3567 format!("{err}").contains("system path"),
3568 "{sys}: error must mention 'system path', got: {err}"
3569 );
3570 }
3571 }
3572
3573 #[test]
3574 fn validate_accepts_safe_watched_folder() {
3575 let s = Settings {
3576 watched_folder: Some(PathBuf::from("/tmp/irontide-watched")),
3577 ..Settings::default()
3578 };
3579 s.validate().expect("safe path must validate");
3580 }
3581}