Skip to main content

irontide_settings/
schema.rs

1//! Single source of truth (SSOT) for engine settings' reconfiguration class +
2//! qBt wire-name. Emitter macros in `irontide-session` project these into
3//! `classify_immediate` / `classify_restart_required`. The `Settings` struct,
4//! `SettingsDelta`, and `validate()` are NOT projected here (see the M247a plan).
5//!
6//! Entry grammar (every key required, in order):
7//!   { name: <ident>, class: <immediate|restart|stored>, wire: <"name"|"">, doc: <"…"> }
8//! `class: immediate` ⇒ emitted into classify_immediate (pushes `wire` on change).
9//! `class: restart`   ⇒ emitted into classify_restart_required.
10//! `class: stored`    ⇒ neither (wire MUST be ""). wire-names unique across all.
11//!
12//! Coverage (M247a): EVERY field of `Settings` (192), `QbtCompatSettings` (20),
13//! and `ProxyConfig` (9) is registered — the complete registry is what makes the
14//! exhaustive-destructuring completeness lock (struct↔registry drift = compile
15//! error) real. Only `class:immediate`/`restart` entries carry a wire-name and
16//! feed the projections; `class:stored` entries exist for the lock + future
17//! M247b struct/delta codegen.
18
19/// Top-level `Settings` fields (192). Invoke with an emitter: `for_each_setting!(emit_x);`
20#[macro_export]
21macro_rules! for_each_setting {
22    ($emit:ident) => {
23        $emit! {
24            // ── General ──
25            { name: listen_port, class: immediate, wire: "listen_port",
26              doc: "TCP listen port for incoming peer connections (default: 42020)." }
27            { name: randomize_port_on_startup, class: stored, wire: "",
28              doc: "Randomize the listen port each time the session starts. Default: false." }
29            { name: download_dir, class: restart, wire: "save_path",
30              doc: "Default download directory for new torrents (default: \".\")." }
31            { name: max_torrents, class: stored, wire: "",
32              doc: "Maximum number of concurrent torrents (default: 100)." }
33            { name: resume_data_dir, class: stored, wire: "",
34              doc: "Directory for fast-resume data files. If `None`, resume data is not persisted." }
35            { name: save_resume_interval_secs, class: immediate, wire: "save_resume_interval",
36              doc: "Interval in seconds between periodic resume file saves (0 = disabled)." }
37            // ── Protocol features ──
38            { name: enable_dht, class: immediate, wire: "dht",
39              doc: "Enable Kademlia DHT peer discovery (BEP 5). Default: true." }
40            { name: enable_pex, class: restart, wire: "pex",
41              doc: "Enable Peer Exchange (BEP 11). Default: true." }
42            { name: enable_lsd, class: immediate, wire: "lsd",
43              doc: "Enable Local Service Discovery via multicast (BEP 14). Default: true." }
44            { name: enable_fast_extension, class: stored, wire: "",
45              doc: "Enable BEP 6 Fast Extension (`AllowedFast`, `HaveAll`, `HaveNone`, Reject," }
46            { name: enable_utp, class: stored, wire: "",
47              doc: "Enable uTP (BEP 29) micro transport protocol. When enabled, outbound" }
48            { name: enable_upnp, class: restart, wire: "upnp",
49              doc: "Enable `UPnP` IGD port mapping (last resort after PCP and NAT-PMP)." }
50            { name: enable_natpmp, class: restart, wire: "natpmp",
51              doc: "Enable NAT-PMP (RFC 6886) and PCP (RFC 6887) port mapping." }
52            { name: enable_ipv6, class: stored, wire: "",
53              doc: "Enable IPv6 dual-stack support (BEP 7, 24). Binds listeners on both" }
54            { name: enable_web_seed, class: stored, wire: "",
55              doc: "Enable HTTP/web seeding (BEP 19 `GetRight`, BEP 17 Hoffman). Torrents" }
56            { name: enable_holepunch, class: stored, wire: "",
57              doc: "Enable BEP 55 holepunch extension for NAT traversal. Advertises" }
58            { name: enable_bep40_eviction, class: stored, wire: "",
59              doc: "Enable BEP 40 canonical peer priority for connection eviction." }
60            { name: enable_diagnostic_counters, class: stored, wire: "",
61              doc: "Enable diagnostic counters (dispatch timing, backpressure high-water," }
62            { name: encryption_mode, class: restart, wire: "encryption",
63              doc: "Connection encryption mode (MSE/PE). Default: Disabled." }
64            { name: anonymous_mode, class: restart, wire: "anonymous_mode",
65              doc: "Suppress identifying information (client version in BEP 10 handshake)" }
66            { name: external_ip, class: stored, wire: "",
67              doc: "Manually configured external IP for BEP 40 peer priority." }
68            // ── Seeding ──
69            { name: seed_ratio_limit, class: immediate, wire: "max_ratio",
70              doc: "Stop seeding when this upload/download ratio is reached. `None` = unlimited." }
71            { name: seed_time_limit_secs, class: immediate, wire: "max_seeding_time",
72              doc: "M171: Stop seeding after this many cumulative seeding seconds." }
73            { name: inactive_seed_time_limit_secs, class: immediate, wire: "max_inactive_seeding_time",
74              doc: "M171: Stop seeding after this many seconds of inactivity while in the" }
75            { name: max_ratio_action, class: immediate, wire: "max_ratio_act",
76              doc: "M171: What to do when `seed_ratio_limit` is reached." }
77            { name: create_subfolder, class: immediate, wire: "create_subfolder_enabled",
78              doc: "M171: Create a subfolder named after the torrent when adding a" }
79            { name: auto_manage_torrents, class: immediate, wire: "auto_tmm_enabled",
80              doc: "M171: Automatically manage torrent resources via the queueing" }
81            { name: queueing_enabled, class: immediate, wire: "queueing_enabled",
82              doc: "M171: Enable the download/upload queueing subsystem. When `false`," }
83            { name: default_super_seeding, class: stored, wire: "",
84              doc: "Enable BEP 16 super seeding for new torrents. Reveals pieces one-per-peer" }
85            { name: default_share_mode, class: stored, wire: "",
86              doc: "Default share mode for new torrents. When true, torrents relay pieces" }
87            { name: upload_only_announce, class: stored, wire: "",
88              doc: "Advertise upload-only status via extension handshake when a torrent" }
89            // ── Rate limiting ──
90            { name: upload_rate_limit, class: immediate, wire: "up_limit",
91              doc: "Global upload rate limit in bytes/sec (0 = unlimited)." }
92            { name: download_rate_limit, class: immediate, wire: "dl_limit",
93              doc: "Global download rate limit in bytes/sec (0 = unlimited)." }
94            { name: tcp_upload_rate_limit, class: stored, wire: "",
95              doc: "TCP upload rate limit in bytes/sec (0 = unlimited)." }
96            { name: tcp_download_rate_limit, class: stored, wire: "",
97              doc: "TCP download rate limit in bytes/sec (0 = unlimited)." }
98            { name: utp_upload_rate_limit, class: stored, wire: "",
99              doc: "uTP upload rate limit in bytes/sec (0 = unlimited)." }
100            { name: utp_download_rate_limit, class: stored, wire: "",
101              doc: "uTP download rate limit in bytes/sec (0 = unlimited)." }
102            { name: auto_upload_slots, class: stored, wire: "",
103              doc: "Automatically adjust the number of upload slots based on bandwidth. Default: true." }
104            { name: auto_upload_slots_min, class: stored, wire: "",
105              doc: "Minimum number of automatic upload slots (default: 2)." }
106            { name: auto_upload_slots_max, class: stored, wire: "",
107              doc: "Maximum number of automatic upload slots (default: 20)." }
108            { name: max_upload_slots_global, class: stored, wire: "",
109              doc: "Maximum upload slots across all torrents (-1 = unlimited). Default: -1." }
110            { name: max_upload_slots_per_torrent, class: stored, wire: "",
111              doc: "Maximum upload slots per torrent. Default: 4." }
112            { name: max_connections_global, class: immediate, wire: "max_connec_global",
113              doc: "Maximum peer connections across all torrents (-1 = unlimited). Default: -1." }
114            { name: max_uploads_per_torrent, class: immediate, wire: "max_uploads_per_torrent",
115              doc: "Maximum unchoked upload slots per torrent (-1 = unlimited). Default: -1." }
116            { name: alt_download_rate_limit, class: stored, wire: "",
117              doc: "Alternative download rate limit in bytes/sec (0 = unlimited)." }
118            { name: alt_upload_rate_limit, class: stored, wire: "",
119              doc: "Alternative upload rate limit in bytes/sec (0 = unlimited)." }
120            { name: alt_speed_enabled, class: stored, wire: "",
121              doc: "Whether alternative speed limits are currently active. Default: false." }
122            { name: alt_speed_schedule_enabled, class: stored, wire: "",
123              doc: "Whether the alternative speed schedule is enabled. Default: false." }
124            { name: alt_speed_schedule_from, class: stored, wire: "",
125              doc: "Schedule start time in minutes-of-day (0-1439). Default: 0." }
126            { name: alt_speed_schedule_to, class: stored, wire: "",
127              doc: "Schedule end time in minutes-of-day (0-1439). Default: 0." }
128            { name: alt_speed_schedule_days, class: stored, wire: "",
129              doc: "Schedule active days as a bitmask (bit 0 = Mon .. bit 6 = Sun). Default: 0." }
130            { name: rate_limit_includes_overhead, class: stored, wire: "",
131              doc: "Include protocol overhead in rate limit calculations. Default: true." }
132            { name: rate_limit_utp, class: stored, wire: "",
133              doc: "Apply rate limits to uTP connections. Default: true." }
134            { name: rate_limit_lan, class: stored, wire: "",
135              doc: "Apply rate limits to LAN connections. Default: false." }
136            { name: mixed_mode_algorithm, class: stored, wire: "",
137              doc: "Mixed-mode TCP/uTP bandwidth allocation algorithm." }
138            // ── Queue management ──
139            { name: active_downloads, class: stored, wire: "",
140              doc: "Maximum concurrent auto-managed downloading torrents (-1 = unlimited, default: 3)." }
141            { name: active_seeds, class: stored, wire: "",
142              doc: "Maximum concurrent auto-managed seeding torrents (-1 = unlimited, default: 5)." }
143            { name: active_limit, class: stored, wire: "",
144              doc: "Hard cap on all active auto-managed torrents (-1 = unlimited, default: 500)." }
145            { name: active_checking, class: stored, wire: "",
146              doc: "Maximum concurrent hash-check operations (default: 1)." }
147            { name: dont_count_slow_torrents, class: stored, wire: "",
148              doc: "Exempt inactive torrents from download/seed limits. A torrent is inactive" }
149            { name: inactive_down_rate, class: stored, wire: "",
150              doc: "Download rate threshold (bytes/sec) below which a torrent is considered" }
151            { name: inactive_up_rate, class: stored, wire: "",
152              doc: "Upload rate threshold (bytes/sec) below which a torrent is considered" }
153            { name: auto_manage_interval, class: stored, wire: "",
154              doc: "Interval in seconds between queue evaluations (default: 30)." }
155            { name: auto_manage_startup, class: stored, wire: "",
156              doc: "Grace period in seconds where a torrent is considered active regardless" }
157            { name: auto_manage_prefer_seeds, class: stored, wire: "",
158              doc: "Allocate seeding slots before download slots. Default: false." }
159            { name: queue_rate_ewma_alpha, class: stored, wire: "",
160              doc: "EWMA smoothing factor for queue rate classification (0.0–1.0)." }
161            { name: seed_queue_min_active_secs, class: stored, wire: "",
162              doc: "Anti-flap grace period for seeding torrents, in seconds." }
163            // ── Alerts ──
164            { name: alert_mask, class: stored, wire: "",
165              doc: "Bitmask of alert categories to receive (default: ALL)." }
166            { name: alert_channel_size, class: stored, wire: "",
167              doc: "Capacity of the alert broadcast channel (default: 1024)." }
168            // ── Smart banning ──
169            { name: smart_ban_max_failures, class: stored, wire: "",
170              doc: "Number of hash-failure involvements before a peer is auto-banned." }
171            { name: smart_ban_parole, class: stored, wire: "",
172              doc: "Enable parole mode: re-download a failed piece from a single uninvolved" }
173            // ── Disk I/O ──
174            { name: disk_io_threads, class: stored, wire: "",
175              doc: "Number of concurrent disk I/O threads (default: 4)." }
176            { name: max_blocking_threads, class: stored, wire: "",
177              doc: "Maximum number of concurrent blocking I/O operations dispatched via" }
178            { name: storage_mode, class: stored, wire: "",
179              doc: "Storage allocation mode: Auto, `FullPreallocate`, or `SparseFile` (default: Auto)." }
180            { name: preallocate_mode, class: stored, wire: "",
181              doc: "Override pre-allocation strategy (None/Sparse/Full). When `None` (default)," }
182            { name: disk_cache_size, class: stored, wire: "",
183              doc: "Total ARC disk cache size in bytes (default: 16 MiB, minimum: 1 MiB)." }
184            { name: disk_write_cache_ratio, class: stored, wire: "",
185              doc: "Fraction of disk cache reserved for write buffering (0.0–1.0, default: 0.5)." }
186            { name: disk_channel_capacity, class: stored, wire: "",
187              doc: "Capacity of the async disk I/O command channel (default: 512)." }
188            { name: buffer_pool_capacity, class: stored, wire: "",
189              doc: "Unified buffer pool capacity in bytes (default: 64 MiB)." }
190            { name: enable_mlock, class: stored, wire: "",
191              doc: "Lock cached piece data in physical memory (default: true on Unix)." }
192            { name: io_uring_sq_depth, class: stored, wire: "",
193              doc: "`io_uring` submission queue depth (number of SQEs). Only used when" }
194            { name: io_uring_direct_io, class: stored, wire: "",
195              doc: "Enable `O_DIRECT` for `io_uring` writes, bypassing the kernel page cache." }
196            { name: filesystem_direct_io, class: stored, wire: "",
197              doc: "Enable direct I/O for filesystem storage (bypasses kernel page cache)." }
198            { name: io_uring_batch_threshold, class: stored, wire: "",
199              doc: "Minimum number of file segments to batch before using `io_uring`." }
200            { name: iocp_concurrent_threads, class: stored, wire: "",
201              doc: "IOCP concurrent thread count (0 = system default). Only used when" }
202            { name: iocp_direct_io, class: stored, wire: "",
203              doc: "Enable `FILE_FLAG_NO_BUFFERING` for IOCP I/O, bypassing the OS page cache." }
204            // ── Hashing & piece picking ──
205            { name: hashing_threads, class: immediate, wire: "hashing_threads",
206              doc: "Number of concurrent piece hash verification threads (default: 2)." }
207            { name: max_request_queue_depth, class: stored, wire: "",
208              doc: "Maximum per-peer request queue depth (default: 250)." }
209            { name: initial_queue_depth, class: stored, wire: "",
210              doc: "Initial per-peer request queue depth (default: 128). Higher values let" }
211            { name: request_queue_time, class: stored, wire: "",
212              doc: "Request queue time multiplier in seconds (default: 3.0)." }
213            { name: block_request_timeout_secs, class: stored, wire: "",
214              doc: "Block request timeout in seconds before the request is considered" }
215            { name: max_concurrent_stream_reads, class: stored, wire: "",
216              doc: "Maximum concurrent `FileStream` readers. Controls how many simultaneous" }
217            { name: auto_sequential, class: stored, wire: "",
218              doc: "Automatically switch to sequential piece picking when too many partial" }
219            { name: strict_end_game, class: stored, wire: "",
220              doc: "In end-game mode, cancel duplicate requests when a piece completes." }
221            { name: max_web_seeds, class: stored, wire: "",
222              doc: "Maximum concurrent web seed connections per torrent (default: 4)." }
223            { name: web_seed_retry_base_secs, class: stored, wire: "",
224              doc: "M186: Base delay (seconds) for web seed exponential backoff. Default: 10." }
225            { name: web_seed_retry_factor, class: stored, wire: "",
226              doc: "M186: Multiplier for web seed exponential backoff. Default: 6." }
227            { name: web_seed_retry_cap_secs, class: stored, wire: "",
228              doc: "M186: Maximum backoff (seconds) for web seed retry. Default: 3600." }
229            { name: web_seed_max_failures, class: stored, wire: "",
230              doc: "M186: Consecutive failures before permanently banning a web seed. Default: 10." }
231            { name: initial_picker_threshold, class: stored, wire: "",
232              doc: "Completed piece count below which the picker uses random selection" }
233            { name: whole_pieces_threshold, class: stored, wire: "",
234              doc: "Seconds to download a piece — if a peer is faster, it gets exclusive" }
235            { name: snub_timeout_secs, class: stored, wire: "",
236              doc: "Seconds without data from a peer before marking it as snubbed." }
237            { name: readahead_pieces, class: stored, wire: "",
238              doc: "Number of pieces ahead of the streaming cursor to prioritize (default: 8)." }
239            { name: streaming_timeout_escalation, class: stored, wire: "",
240              doc: "Escalate streaming piece requests that exceed the mean RTT. Default: true." }
241            { name: steal_threshold_ratio, class: stored, wire: "",
242              doc: "Steal blocks from peers this many times slower than the requesting peer (default: 10.0)." }
243            { name: use_block_stealing, class: stored, wire: "",
244              doc: "Enable per-block stealing: fast peers can steal individual unrequested" }
245            { name: steal_stale_piece_secs, class: stored, wire: "",
246              doc: "Seconds between steal-queue population scans. Every N seconds, all" }
247            { name: steal_threshold_endgame, class: stored, wire: "",
248              doc: "M149: Steal threshold multiplier when >90% complete (endgame)." }
249            { name: fixed_pipeline_depth, class: stored, wire: "",
250              doc: "Fixed per-peer pipeline depth (number of concurrent requests per peer)." }
251            // ── Piece picker enhancements (M44) ──
252            { name: piece_extent_affinity, class: stored, wire: "",
253              doc: "Prefer pieces adjacent to those already downloaded for improved sequential" }
254            { name: suggest_mode, class: stored, wire: "",
255              doc: "Enable BEP 6 `SuggestPiece`: suggest newly verified pieces to peers that" }
256            { name: max_suggest_pieces, class: stored, wire: "",
257              doc: "Maximum `SuggestPiece` messages per peer to avoid flooding (default: 10)." }
258            { name: predictive_piece_announce_ms, class: stored, wire: "",
259              doc: "Predictive piece announce delay in milliseconds. When > 0, a Have message" }
260            // ── Proxy ──
261            { name: proxy, class: stored, wire: "",
262              doc: "Proxy configuration for peer and tracker connections. Default: no proxy." }
263            { name: force_proxy, class: restart, wire: "force_proxy",
264              doc: "Force all connections through the configured proxy. Disables listen" }
265            // ── IP Filtering ──
266            { name: ip_filter_enabled, class: immediate, wire: "ip_filter_enabled",
267              doc: "Enable the IP filter (blocklist). Default: false." }
268            { name: ip_filter_path, class: stored, wire: "",
269              doc: "Path to the IP filter file (e.g. `ipfilter.dat`). Default: empty." }
270            { name: ip_filter_auto_refresh, class: immediate, wire: "ip_filter_auto_refresh",
271              doc: "Automatically refresh the IP filter when the file changes. Default: false." }
272            { name: apply_ip_filter_to_trackers, class: stored, wire: "",
273              doc: "Check tracker IP addresses against the IP filter. When false, trackers" }
274            // ── DHT tuning ──
275            { name: dht_queries_per_second, class: stored, wire: "",
276              doc: "Maximum DHT queries per second to control network traffic (default: 50)." }
277            { name: dht_query_timeout_secs, class: stored, wire: "",
278              doc: "Timeout in seconds for a single DHT query before it is abandoned (default: 5)." }
279            { name: dht_enforce_node_id, class: stored, wire: "",
280              doc: "BEP 42: Enforce node ID verification in DHT routing table." }
281            { name: dht_restrict_routing_ips, class: stored, wire: "",
282              doc: "BEP 42: Restrict DHT routing table to one node per IP." }
283            { name: dht_max_items, class: stored, wire: "",
284              doc: "Maximum number of BEP 44 items stored in the DHT (immutable + mutable)." }
285            { name: dht_item_lifetime_secs, class: stored, wire: "",
286              doc: "Lifetime of BEP 44 DHT items in seconds before expiry (default: 7200 = 2 hours)." }
287            { name: dht_sample_infohashes_interval, class: stored, wire: "",
288              doc: "Interval in seconds for periodic `sample_infohashes` queries (BEP 51)." }
289            { name: dht_read_only, class: stored, wire: "",
290              doc: "BEP 43: Run DHT in read-only mode. Read-only nodes can query the DHT" }
291            // ── NAT tuning ──
292            { name: upnp_lease_duration, class: stored, wire: "",
293              doc: "`UPnP` lease duration in seconds (default: 3600)." }
294            { name: natpmp_lifetime, class: stored, wire: "",
295              doc: "NAT-PMP mapping lifetime in seconds (default: 7200)." }
296            // ── uTP tuning ──
297            { name: utp_max_connections, class: stored, wire: "",
298              doc: "Maximum concurrent uTP connections (default: 256)." }
299            // ── I2P ──
300            { name: enable_i2p, class: stored, wire: "",
301              doc: "Enable I2P anonymous network support (requires SAM bridge)." }
302            { name: i2p_hostname, class: stored, wire: "",
303              doc: "SAM bridge hostname (default: \"127.0.0.1\")." }
304            { name: i2p_port, class: stored, wire: "",
305              doc: "SAM bridge port (default: 7656)." }
306            { name: i2p_inbound_quantity, class: stored, wire: "",
307              doc: "Number of inbound I2P tunnels (1-16, default: 3)." }
308            { name: i2p_outbound_quantity, class: stored, wire: "",
309              doc: "Number of outbound I2P tunnels (1-16, default: 3)." }
310            { name: i2p_inbound_length, class: stored, wire: "",
311              doc: "Number of hops in inbound I2P tunnels (0-7, default: 3)." }
312            { name: i2p_outbound_length, class: stored, wire: "",
313              doc: "Number of hops in outbound I2P tunnels (0-7, default: 3)." }
314            { name: allow_i2p_mixed, class: stored, wire: "",
315              doc: "Allow mixing I2P and clearnet peers in the same torrent." }
316            // ── SSL torrents (M42) ──
317            { name: ssl_listen_port, class: stored, wire: "",
318              doc: "SSL listen port for SSL torrent incoming connections." }
319            { name: ssl_cert_path, class: stored, wire: "",
320              doc: "Path to the PEM-encoded certificate file for SSL torrent connections." }
321            { name: ssl_key_path, class: stored, wire: "",
322              doc: "Path to the PEM-encoded private key file for SSL torrent connections." }
323            // ── Choking algorithms (M43) ──
324            { name: seed_choking_algorithm, class: stored, wire: "",
325              doc: "Algorithm for ranking peers during seed-mode choking." }
326            { name: choking_algorithm, class: stored, wire: "",
327              doc: "Algorithm for determining the number of unchoke slots." }
328            // ── Peer connections ──
329            { name: max_peers_per_torrent, class: immediate, wire: "max_connec",
330              doc: "Maximum peer connections per torrent (default: 128). When `0`, falls back" }
331            { name: pass0_grace_secs, class: stored, wire: "",
332              doc: "v0.187.3 / OV2 / 12A: seconds after a peer goes Live before Pass 0" }
333            { name: proactive_evictions_per_minute_limit, class: stored, wire: "",
334              doc: "v0.187.3 / 3A: sliding-window cap on proactive evictions in any" }
335            { name: eviction_ban_duration_secs, class: stored, wire: "",
336              doc: "v0.187.3: how long a Pass 0 eviction victim is blocked from" }
337            { name: eviction_ban_set_cap, class: stored, wire: "",
338              doc: "v0.187.3 / OV4: FIFO cap on the banned-peer set. Default 1024 (was" }
339            { name: peer_read_timeout_secs, class: stored, wire: "",
340              doc: "M133: Seconds without any wire message before disconnecting a peer." }
341            { name: peer_write_timeout_secs, class: stored, wire: "",
342              doc: "M133: Seconds before a stalled outgoing write disconnects a peer." }
343            { name: data_contribution_timeout_secs, class: stored, wire: "",
344              doc: "M137: Data contribution timeout — seconds without receiving a Piece" }
345            { name: choke_rotation_max_evictions, class: stored, wire: "",
346              doc: "M138: Maximum peers to evict per choke rotation tick (0 = disabled)." }
347            { name: max_concurrent_connects, class: stored, wire: "",
348              doc: "M138: Maximum concurrent outbound peer connections (throttles connect ramp)." }
349            { name: connect_soft_timeout, class: stored, wire: "",
350              doc: "M147: Seconds without TCP SYN-ACK before soft reap disconnects a connecting" }
351            { name: dispatch_backlog_cap, class: stored, wire: "",
352              doc: "M182: dispatch-channel backlog cap. The reader's `BackpressureQueue`" }
353            { name: event_backlog_cap, class: stored, wire: "",
354              doc: "M182: event-channel backlog cap. Same role as" }
355            { name: peer_writer_channel_cap, class: stored, wire: "",
356              doc: "M243 (L6): per-peer outbound writer channel capacity. The writer" }
357            { name: use_actor_dispatch, class: stored, wire: "",
358              doc: "M187 A/B: use actor-centralised dispatch (true) or per-peer CAS dispatch (false)." }
359            { name: web_seed_progress_throttle_ms, class: stored, wire: "",
360              doc: "M178: Minimum milliseconds between `PeerEvent::WebSeedProgress` emissions" }
361            // ── Security ──
362            { name: ssrf_mitigation, class: stored, wire: "",
363              doc: "Enable SSRF mitigation: restrict localhost tracker paths, block" }
364            { name: allow_idna, class: stored, wire: "",
365              doc: "Allow internationalised (non-ASCII) domain names in tracker/web seed URLs." }
366            { name: validate_https_trackers, class: stored, wire: "",
367              doc: "Require HTTPS for HTTP tracker announces (UDP trackers are unaffected)." }
368            { name: max_metadata_size, class: stored, wire: "",
369              doc: "Maximum BEP 9 metadata size in bytes that will be accepted from peers." }
370            { name: max_message_size, class: stored, wire: "",
371              doc: "Maximum wire protocol message size in bytes. Messages exceeding this are" }
372            { name: max_piece_length, class: stored, wire: "",
373              doc: "Maximum accepted piece length when adding a torrent. Rejects torrents" }
374            { name: max_outstanding_requests, class: stored, wire: "",
375              doc: "Maximum outstanding incoming requests per peer. When a peer sends more" }
376            { name: max_in_flight_pieces, class: stored, wire: "",
377              doc: "Maximum number of pieces simultaneously in-flight (downloaded but not" }
378            { name: peer_connect_timeout, class: stored, wire: "",
379              doc: "Timeout in seconds for outbound TCP peer connections." }
380            { name: peer_dscp, class: stored, wire: "",
381              doc: "DSCP (Differentiated Services Code Point) value for peer traffic sockets." }
382            // ── Session Stats (M50) ──
383            { name: stats_report_interval, class: stored, wire: "",
384              doc: "Interval in milliseconds between `SessionStatsAlert` emissions." }
385            // ── Runtime tuning (M95) ──
386            { name: runtime_worker_threads, class: stored, wire: "",
387              doc: "Number of tokio worker threads. Default: min(available cores, 8)." }
388            { name: pin_cores, class: stored, wire: "",
389              doc: "Pin tokio worker threads to CPU cores for cache locality. Default: true." }
390            // ── Lock diagnostics (M120) ──
391            { name: lock_warn_threshold_ms, class: stored, wire: "",
392              doc: "Warning threshold in milliseconds for lock hold duration." }
393            // ── DHT bootstrap (M56) ──
394            { name: dht_saved_nodes, class: stored, wire: "",
395              doc: "Previously saved DHT routing table nodes for fast bootstrap." }
396            { name: dht_node_id, class: stored, wire: "",
397              doc: "BEP 42-compliant DHT node ID from previous session." }
398            { name: qbt_compat, class: stored, wire: "",
399              doc: "qBittorrent `WebUI` v2 compatibility layer (M168)." }
400            { name: category_registry_path, class: stored, wire: "",
401              doc: "M170: Path to the qBt-compat category registry TOML file. When" }
402            { name: tag_registry_path, class: stored, wire: "",
403              doc: "M171: Path to the qBt-compat tag registry TOML file. When `None`," }
404            // ── M226: Notifications / paths / watched folder / network ──
405            { name: notify_on_complete, class: immediate, wire: "notify_on_complete",
406              doc: "M226: Fire an OS desktop notification when a torrent finishes" }
407            { name: notify_on_error, class: immediate, wire: "notify_on_error",
408              doc: "M226: Fire an OS desktop notification when a torrent enters an error" }
409            { name: on_complete_program, class: immediate, wire: "on_complete_program",
410              doc: "M226: Path to a program to run on torrent completion (qBt parity" }
411            { name: use_incomplete_dir, class: immediate, wire: "use_incomplete_dir",
412              doc: "M226: Whether in-progress downloads use a separate directory before" }
413            { name: incomplete_dir, class: immediate, wire: "incomplete_dir",
414              doc: "M226: Directory for in-progress downloads (paired with" }
415            { name: default_skip_hash_check, class: immediate, wire: "default_skip_hash_check",
416              doc: "M226: Default value for `AddTorrentParams.skip_checking` when the" }
417            { name: incomplete_extension_enabled, class: immediate, wire: "incomplete_extension_enabled",
418              doc: "M226: Append `.!ut` to filenames during download (qBt convention to" }
419            { name: watched_folder, class: immediate, wire: "watched_folder",
420              doc: "M226: Path to a folder to watch for new `.torrent` files; on detection" }
421            { name: delete_torrent_after_add, class: immediate, wire: "delete_torrent_after_add",
422              doc: "M226: After successfully adding a `.torrent` file from `watched_folder`," }
423            { name: move_completed_enabled, class: immediate, wire: "move_completed_enabled",
424              doc: "M226: Whether to move completed torrents to `move_completed_to`." }
425            { name: move_completed_to, class: immediate, wire: "move_completed_to",
426              doc: "M226: Destination for completed torrents (paired with" }
427            { name: web_ui_https_enabled, class: immediate, wire: "web_ui_https_enabled",
428              doc: "M226: Enable `HTTPS` for the qBt v2 `WebUI` listener. STORED ONLY —" }
429            { name: network_interface, class: immediate, wire: "network_interface",
430              doc: "M226: Bind peer listeners to a specific network interface (qBt" }
431            { name: default_add_paused, class: immediate, wire: "default_add_paused",
432              doc: "M226: When `AddTorrentParams.paused` is `None`, this default decides" }
433        }
434    };
435}
436
437/// Nested `QbtCompatSettings` fields (20; accessed via `self.qbt_compat.<name>`).
438#[macro_export]
439macro_rules! for_each_qbt_compat_setting {
440    ($emit:ident) => {
441        $emit! {
442            { name: enabled, class: stored, wire: "",
443              doc: "Master enable flag. When `false`, all `/api/v2/*` routes return 404" }
444            { name: username, class: stored, wire: "",
445              doc: "Username required for qBt v2 login. Default: `\"admin\"` (qBt factory" }
446            { name: password_hash, class: stored, wire: "",
447              doc: "Argon2id PHC-format password hash (M172a). Example:" }
448            { name: password, class: stored, wire: "",
449              doc: "Legacy plaintext password — **deprecated, migration-only**. Populated" }
450            { name: spoof_app_version, class: stored, wire: "",
451              doc: "Version string returned by `GET /api/v2/app/version`. Must match the" }
452            { name: spoof_webapi_version, class: stored, wire: "",
453              doc: "Version string returned by `GET /api/v2/app/webapiVersion`. Must match" }
454            { name: session_ttl_secs, class: stored, wire: "",
455              doc: "Session cookie TTL in seconds. Bounds: `[60, 604_800]` (1 minute to" }
456            { name: max_sessions, class: stored, wire: "",
457              doc: "Maximum concurrent sessions. Prevents unbounded growth on login" }
458            { name: max_concurrent_argon2_ops, class: stored, wire: "",
459              doc: "Optional override for the global argon2 verification semaphore size" }
460            { name: port, class: restart, wire: "webui_port",
461              doc: "v0.187.3 / 2A: TCP port the Web UI listens on. Single source of truth" }
462            { name: bind_address, class: restart, wire: "webui_bind",
463              doc: "v0.187.3 / 2A: bind address the Web UI listens on. Single source of" }
464            { name: csrf_protection_enabled, class: immediate, wire: "web_ui_csrf_protection_enabled",
465              doc: "M172a Lane B: enable Origin/Referer CSRF checks on mutating requests" }
466            { name: host_header_validation_enabled, class: immediate, wire: "web_ui_host_header_validation_enabled",
467              doc: "M172a Lane B: enable Host-header validation against Origin/Referer." }
468            { name: web_ui_reverse_proxy_enabled, class: immediate, wire: "web_ui_reverse_proxy_enabled",
469              doc: "M172a Lane B: when true, the CSRF middleware resolves the real client" }
470            { name: web_ui_reverse_proxies_list, class: immediate, wire: "web_ui_reverse_proxies_list",
471              doc: "M172a Lane B: list of CIDRs trusted to supply `X-Forwarded-For` and" }
472            // ── M172a Lane C: brute-force ban ──
473            { name: max_failed_auth_count, class: immediate, wire: "web_ui_max_auth_fail_count",
474              doc: "Maximum number of failed `auth/login` attempts from a single source" }
475            { name: ban_duration_secs, class: immediate, wire: "web_ui_ban_duration",
476              doc: "Ban duration (seconds) after hitting [`Self::max_failed_auth_count`]." }
477            { name: bypass_local_auth, class: immediate, wire: "bypass_local_auth",
478              doc: "When `true`, any request whose resolved client IP is loopback" }
479            { name: bypass_auth_subnet_whitelist, class: immediate, wire: "bypass_auth_subnet_whitelist",
480              doc: "CIDR strings whose resolved client IP bypasses authentication" }
481            { name: brute_force_registry_capacity, class: immediate, wire: "brute_force_registry_capacity",
482              doc: "Optional override for the brute-force-ban registry's LRU capacity." }
483        }
484    };
485}
486
487/// Nested `ProxyConfig` fields (9; accessed via `self.proxy.<name>`).
488#[macro_export]
489macro_rules! for_each_proxy_setting {
490    ($emit:ident) => {
491        $emit! {
492            { name: proxy_type, class: restart, wire: "proxy_type",
493              doc: "Proxy protocol to use." }
494            { name: hostname, class: restart, wire: "proxy_ip",
495              doc: "Proxy server hostname or IP address." }
496            { name: port, class: restart, wire: "proxy_port",
497              doc: "Proxy server port." }
498            { name: username, class: restart, wire: "proxy_username",
499              doc: "Username for authenticated proxy types." }
500            { name: password, class: restart, wire: "proxy_password",
501              doc: "Password for authenticated proxy types." }
502            { name: proxy_peer_connections, class: restart, wire: "proxy_peer_connections",
503              doc: "Route peer connections (incl. web seeds) through proxy." }
504            { name: proxy_tracker_connections, class: stored, wire: "",
505              doc: "Route tracker HTTP connections through proxy." }
506            { name: proxy_hostnames, class: restart, wire: "proxy_hostnames",
507              doc: "Resolve hostnames through proxy (SOCKS5/HTTP only)." }
508            { name: socks5_udp_send_local_ep, class: stored, wire: "",
509              doc: "Include local endpoint in SOCKS5 UDP ASSOCIATE." }
510        }
511    };
512}
513
514/// Live-reconfigurable settings projected into `SettingsDelta` (28) — the M247b
515/// SSOT for `SettingsDelta::from_diff` / `is_empty`. The subset of `Settings`
516/// propagated to running torrents by `apply_settings`. Per entry: the source
517/// `Settings` field (`src`), the `SettingsDelta` field name (`delta`, differs
518/// only for `max_peers`), and whether `from_diff` clones the value (`clone:
519/// true` for the non-`Copy` `PathBuf`/`String` payloads). Type metadata is
520/// intentionally absent: the `SettingsDelta` struct stays hand-written (M247b
521/// plan D3), so only diff/emptiness projection is generated. Entry order matches
522/// `from_diff` for readability; correctness does not depend on it (distinct
523/// fields). Invoke with an emitter: `for_each_delta_setting!(emit_x);`
524#[macro_export]
525macro_rules! for_each_delta_setting {
526    ($emit:ident) => {
527        $emit! {
528            { src: enable_dht,                    delta: enable_dht,                    clone: false }
529            { src: enable_pex,                    delta: enable_pex,                    clone: false }
530            { src: max_peers_per_torrent,         delta: max_peers,                     clone: false }
531            { src: seed_ratio_limit,              delta: seed_ratio_limit,              clone: false }
532            { src: seed_time_limit_secs,          delta: seed_time_limit_secs,          clone: false }
533            { src: inactive_seed_time_limit_secs, delta: inactive_seed_time_limit_secs, clone: false }
534            { src: max_ratio_action,              delta: max_ratio_action,              clone: false }
535            { src: encryption_mode,               delta: encryption_mode,               clone: false }
536            { src: anonymous_mode,                delta: anonymous_mode,                clone: false }
537            { src: max_uploads_per_torrent,       delta: max_uploads_per_torrent,       clone: false }
538            { src: save_resume_interval_secs,     delta: save_resume_interval_secs,     clone: false }
539            { src: hashing_threads,               delta: hashing_threads,               clone: false }
540            { src: ip_filter_enabled,             delta: ip_filter_enabled,             clone: false }
541            { src: notify_on_complete,            delta: notify_on_complete,            clone: false }
542            { src: notify_on_error,               delta: notify_on_error,               clone: false }
543            { src: on_complete_program,           delta: on_complete_program,           clone: true  }
544            { src: use_incomplete_dir,            delta: use_incomplete_dir,            clone: false }
545            { src: incomplete_dir,                delta: incomplete_dir,                clone: true  }
546            { src: default_skip_hash_check,       delta: default_skip_hash_check,       clone: false }
547            { src: incomplete_extension_enabled,  delta: incomplete_extension_enabled,  clone: false }
548            { src: watched_folder,                delta: watched_folder,                clone: true  }
549            { src: delete_torrent_after_add,      delta: delete_torrent_after_add,      clone: false }
550            { src: move_completed_enabled,        delta: move_completed_enabled,        clone: false }
551            { src: move_completed_to,             delta: move_completed_to,             clone: true  }
552            { src: ip_filter_auto_refresh,        delta: ip_filter_auto_refresh,        clone: false }
553            { src: web_ui_https_enabled,          delta: web_ui_https_enabled,          clone: false }
554            { src: network_interface,             delta: network_interface,             clone: true  }
555            { src: default_add_paused,            delta: default_add_paused,            clone: false }
556        }
557    };
558}
559
560// ─────────────────────────────────────────────────────────────────────────
561// Completeness lock (M247a Task 4). Exhaustive struct destructuring generated
562// from the registry — NO `..` arm. A field present in the struct but missing
563// from the registry → E0027 "pattern does not mention field"; a registry entry
564// with no matching struct field → E0026 "does not have a field named". So
565// bidirectional struct↔registry drift is a COMPILE error, not a silent gap.
566// The fns are never called (compile-time type-check only; `&` form sidesteps
567// any move/Drop concern). This is what makes "register EVERY field" load-bearing
568// rather than aspirational.
569// ─────────────────────────────────────────────────────────────────────────
570
571macro_rules! emit_settings_lock {
572    ( $({ name: $name:ident, class: $class:tt, wire: $wire:literal, doc: $doc:literal })* ) => {
573        #[allow(dead_code)]
574        fn _settings_completeness_lock(s: &crate::Settings) {
575            let crate::Settings { $($name: _,)* } = s;
576        }
577    };
578}
579crate::for_each_setting!(emit_settings_lock);
580
581macro_rules! emit_qbt_lock {
582    ( $({ name: $name:ident, class: $class:tt, wire: $wire:literal, doc: $doc:literal })* ) => {
583        #[allow(dead_code)]
584        fn _qbt_completeness_lock(s: &crate::QbtCompatSettings) {
585            let crate::QbtCompatSettings { $($name: _,)* } = s;
586        }
587    };
588}
589crate::for_each_qbt_compat_setting!(emit_qbt_lock);
590
591macro_rules! emit_proxy_lock {
592    ( $({ name: $name:ident, class: $class:tt, wire: $wire:literal, doc: $doc:literal })* ) => {
593        #[allow(dead_code)]
594        fn _proxy_completeness_lock(s: &irontide_core::ProxyConfig) {
595            let irontide_core::ProxyConfig { $($name: _,)* } = s;
596        }
597    };
598}
599crate::for_each_proxy_setting!(emit_proxy_lock);
600
601#[cfg(test)]
602mod registry_consistency {
603    //! Golden guards over the SSOT registry. Replaces the Task-1 mechanism
604    //! tracer — proving the `push_if!`/emitter mechanism is now the job of the
605    //! live `classify_*` golden tests in `irontide-session`. These pin the
606    //! registry's own invariants: class↔wire consistency, wire-name uniqueness,
607    //! and the exact immediate/restart wire-name SETS. Combined with the
608    //! session-crate classify tests (which prove the codegen faithfully projects
609    //! the registry), they transitively pin the cross-repo
610    //! `X-IronTide-Restart-Pending` contract (see `classify_immediate`'s doc).
611    use std::collections::HashSet;
612
613    // Collects (class, name, wire) triples from a registry into a Vec.
614    macro_rules! collect_meta {
615        ( $({ name: $name:ident, class: $class:tt, wire: $wire:literal, doc: $doc:literal })* ) => {{
616            let out: Vec<(&'static str, &'static str, &'static str)> = vec![
617                $( (stringify!($class), stringify!($name), $wire), )*
618            ];
619            out
620        }};
621    }
622
623    fn all_meta() -> Vec<(&'static str, &'static str, &'static str)> {
624        let mut v = crate::for_each_setting!(collect_meta);
625        v.extend(crate::for_each_qbt_compat_setting!(collect_meta));
626        v.extend(crate::for_each_proxy_setting!(collect_meta));
627        v
628    }
629
630    /// Every `immediate`/`restart` entry carries a non-empty wire-name; every
631    /// `stored` entry has an empty wire-name; no other class exists.
632    #[test]
633    fn class_wire_consistency() {
634        for (class, name, wire) in all_meta() {
635            match class {
636                "immediate" | "restart" => assert!(
637                    !wire.is_empty(),
638                    "{name} ({class}) must carry a non-empty wire-name"
639                ),
640                "stored" => assert!(
641                    wire.is_empty(),
642                    "{name} (stored) must have an empty wire-name, got {wire:?}"
643                ),
644                other => panic!("{name}: unknown class `{other}`"),
645            }
646        }
647    }
648
649    /// No two projected fields share a wire-name (a collision would make the
650    /// `X-IronTide-Restart-Pending` header ambiguous downstream).
651    #[test]
652    fn wire_names_unique() {
653        let wires: Vec<&str> = all_meta()
654            .into_iter()
655            .map(|(_, _, w)| w)
656            .filter(|w| !w.is_empty())
657            .collect();
658        let set: HashSet<&str> = wires.iter().copied().collect();
659        assert_eq!(
660            wires.len(),
661            set.len(),
662            "duplicate wire-name in the registry"
663        );
664    }
665
666    /// The set of `immediate` wire-names is exactly the 42 that
667    /// `classify_immediate` projected pre-M247a. Drift here = wire-contract break.
668    #[test]
669    fn immediate_wire_set_pinned() {
670        let got: HashSet<&str> = all_meta()
671            .into_iter()
672            .filter(|(c, _, _)| *c == "immediate")
673            .map(|(_, _, w)| w)
674            .collect();
675        let expected: HashSet<&str> = [
676            // top-level (33)
677            "dl_limit",
678            "up_limit",
679            "max_connec",
680            "max_ratio_act",
681            "create_subfolder_enabled",
682            "auto_tmm_enabled",
683            "queueing_enabled",
684            "max_ratio",
685            "listen_port",
686            "dht",
687            "lsd",
688            "max_connec_global",
689            "max_uploads_per_torrent",
690            "max_seeding_time",
691            "max_inactive_seeding_time",
692            "save_resume_interval",
693            "hashing_threads",
694            "ip_filter_enabled",
695            "notify_on_complete",
696            "notify_on_error",
697            "on_complete_program",
698            "use_incomplete_dir",
699            "incomplete_dir",
700            "default_skip_hash_check",
701            "incomplete_extension_enabled",
702            "watched_folder",
703            "delete_torrent_after_add",
704            "move_completed_enabled",
705            "move_completed_to",
706            "ip_filter_auto_refresh",
707            "web_ui_https_enabled",
708            "network_interface",
709            "default_add_paused",
710            // qbt_compat (9)
711            "web_ui_csrf_protection_enabled",
712            "web_ui_host_header_validation_enabled",
713            "web_ui_reverse_proxy_enabled",
714            "web_ui_reverse_proxies_list",
715            "web_ui_max_auth_fail_count",
716            "web_ui_ban_duration",
717            "bypass_local_auth",
718            "bypass_auth_subnet_whitelist",
719            "brute_force_registry_capacity",
720        ]
721        .into_iter()
722        .collect();
723        assert_eq!(got, expected, "classify_immediate wire-name SET drifted");
724    }
725
726    /// The set of `restart` wire-names is exactly the 16 that
727    /// `classify_restart_required` projected pre-M247a.
728    #[test]
729    fn restart_wire_set_pinned() {
730        let got: HashSet<&str> = all_meta()
731            .into_iter()
732            .filter(|(c, _, _)| *c == "restart")
733            .map(|(_, _, w)| w)
734            .collect();
735        let expected: HashSet<&str> = [
736            // top-level (7)
737            "pex",
738            "encryption",
739            "anonymous_mode",
740            "save_path",
741            "upnp",
742            "natpmp",
743            "force_proxy",
744            // qbt_compat (2)
745            "webui_port",
746            "webui_bind",
747            // proxy (7)
748            "proxy_type",
749            "proxy_ip",
750            "proxy_port",
751            "proxy_username",
752            "proxy_password",
753            "proxy_peer_connections",
754            "proxy_hostnames",
755        ]
756        .into_iter()
757        .collect();
758        assert_eq!(
759            got, expected,
760            "classify_restart_required wire-name SET drifted"
761        );
762    }
763}