1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_precision_loss,
4 clippy::cast_possible_wrap,
5 clippy::cast_sign_loss,
6 reason = "M175: human-readable display formatting — values bounded by realistic torrent sizes; precision loss intentional for short readable strings"
7)]
8
9pub mod file_entries;
16pub mod peer_flags;
17pub mod pseudo_trackers;
18
19pub use file_entries::{FlatFileEntry, build_flat};
20pub use peer_flags::peer_flags;
21pub use pseudo_trackers::{
22 PSEUDO_TRACKER_DHT_URL, PSEUDO_TRACKER_LSD_URL, PSEUDO_TRACKER_PEX_URL, PSEUDO_TRACKER_TIER,
23 is_pseudo_tracker, synthesize_pseudo_trackers,
24};
25
26use irontide_core::FilePriority;
27use irontide_session::TorrentState;
28
29#[must_use]
33pub fn parse_priority_label(s: &str) -> Option<FilePriority> {
34 match s {
35 "skip" => Some(FilePriority::Skip),
36 "low" => Some(FilePriority::Low),
37 "normal" => Some(FilePriority::Normal),
38 "high" => Some(FilePriority::High),
39 _ => None,
40 }
41}
42
43#[must_use]
49pub fn format_size(bytes: u64) -> String {
50 const KIB: u64 = 1024;
51 const MIB: u64 = 1024 * KIB;
52 const GIB: u64 = 1024 * MIB;
53 if bytes >= GIB {
54 format!("{:.2} GiB", bytes as f64 / GIB as f64)
55 } else if bytes >= MIB {
56 format!("{:.1} MiB", bytes as f64 / MIB as f64)
57 } else if bytes >= KIB {
58 format!("{:.1} KiB", bytes as f64 / KIB as f64)
59 } else {
60 format!("{bytes} B")
61 }
62}
63
64#[must_use]
69pub fn format_rate(bytes_per_sec: u64) -> String {
70 const KIB: u64 = 1024;
71 const MIB: u64 = 1024 * KIB;
72 if bytes_per_sec >= MIB {
73 format!("{:.1} MB/s", bytes_per_sec as f64 / MIB as f64)
74 } else if bytes_per_sec >= KIB {
75 format!("{:.1} KB/s", bytes_per_sec as f64 / KIB as f64)
76 } else {
77 format!("{bytes_per_sec} B/s")
78 }
79}
80
81#[must_use]
86pub fn format_state(state: &TorrentState, user_seed_mode: bool) -> &'static str {
87 if user_seed_mode && matches!(state, TorrentState::Downloading) {
88 return "seed only";
89 }
90 match state {
91 TorrentState::FetchingMetadata => "fetching metadata",
92 TorrentState::Checking => "checking",
93 TorrentState::Downloading => "downloading",
94 TorrentState::Complete => "complete",
95 TorrentState::Seeding => "seeding",
96 TorrentState::Paused => "paused",
97 TorrentState::Queued => "queued",
98 TorrentState::Stopped => "stopped",
99 TorrentState::Sharing => "sharing",
100 }
101}
102
103#[must_use]
104pub fn format_state_with_super_seeding(
105 state: &TorrentState,
106 user_seed_mode: bool,
107 super_seeding: bool,
108) -> &'static str {
109 if super_seeding && matches!(state, TorrentState::Seeding) {
110 return "super seeding";
111 }
112 format_state(state, user_seed_mode)
113}
114
115#[must_use]
120pub fn format_ratio(uploaded: u64, downloaded: u64) -> String {
121 if downloaded == 0 && uploaded > 0 {
122 return "\u{221e}".to_string(); }
124 if downloaded == 0 {
125 return "0.00".to_string();
126 }
127 format!("{:.2}", uploaded as f64 / downloaded as f64)
128}
129
130#[must_use]
136pub fn format_eta(remaining_bytes: u64, rate_bps: u64) -> String {
137 if rate_bps == 0 {
138 return "\u{2014}".to_string(); }
140 let secs = remaining_bytes / rate_bps;
141 if secs >= 86400 {
142 let days = secs / 86400;
143 let hours = (secs % 86400) / 3600;
144 format!("{days}d {hours}h")
145 } else if secs >= 3600 {
146 let hours = secs / 3600;
147 let mins = (secs % 3600) / 60;
148 format!("{hours}h {mins}m")
149 } else if secs >= 60 {
150 let mins = secs / 60;
151 let s = secs % 60;
152 format!("{mins}m {s}s")
153 } else {
154 format!("{secs}s")
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_format_size_boundaries() {
164 assert_eq!(format_size(0), "0 B");
166 assert_eq!(format_size(512), "512 B");
168 assert_eq!(format_size(1024), "1.0 KiB");
170 assert_eq!(format_size(512 * 1024), "512.0 KiB");
172 assert_eq!(format_size(1_048_576), "1.0 MiB");
174 assert_eq!(format_size(512 * 1_048_576), "512.0 MiB");
176 assert_eq!(format_size(1_073_741_824), "1.00 GiB");
178 assert_eq!(format_size(512 * 1_073_741_824), "512.00 GiB");
180 }
181
182 #[test]
183 fn test_format_rate_boundaries() {
184 assert_eq!(format_rate(0), "0 B/s");
186 assert_eq!(format_rate(512), "512 B/s");
188 assert_eq!(format_rate(1024), "1.0 KB/s");
190 assert_eq!(format_rate(512 * 1024), "512.0 KB/s");
192 assert_eq!(format_rate(1_048_576), "1.0 MB/s");
194 assert_eq!(format_rate(1_073_741_824), "1024.0 MB/s");
196 }
197
198 #[test]
199 fn test_format_state_all_variants() {
200 assert_eq!(
202 format_state(&TorrentState::FetchingMetadata, false),
203 "fetching metadata"
204 );
205 assert_eq!(format_state(&TorrentState::Checking, false), "checking");
206 assert_eq!(
207 format_state(&TorrentState::Downloading, false),
208 "downloading"
209 );
210 assert_eq!(format_state(&TorrentState::Complete, false), "complete");
211 assert_eq!(format_state(&TorrentState::Seeding, false), "seeding");
212 assert_eq!(format_state(&TorrentState::Paused, false), "paused");
213 assert_eq!(format_state(&TorrentState::Queued, false), "queued");
214 assert_eq!(format_state(&TorrentState::Stopped, false), "stopped");
215 assert_eq!(format_state(&TorrentState::Sharing, false), "sharing");
216
217 assert_eq!(format_state(&TorrentState::Downloading, true), "seed only");
219 assert_eq!(format_state(&TorrentState::Seeding, true), "seeding");
221 assert_eq!(format_state(&TorrentState::Paused, true), "paused");
222 assert_eq!(format_state(&TorrentState::Queued, true), "queued");
223 assert_eq!(format_state(&TorrentState::Complete, true), "complete");
224 assert_eq!(format_state(&TorrentState::Sharing, true), "sharing");
225 assert_eq!(format_state(&TorrentState::Stopped, true), "stopped");
226 }
227
228 #[test]
229 fn test_format_ratio() {
230 assert_eq!(format_ratio(0, 0), "0.00");
232 assert_eq!(format_ratio(500, 0), "\u{221e}");
234 assert_eq!(format_ratio(150, 100), "1.50");
236 assert_eq!(format_ratio(1000, 1000), "1.00");
238 assert_eq!(format_ratio(1_000_000, 1_000), "1000.00");
240 assert_eq!(format_ratio(50, 100), "0.50");
242 }
243
244 #[test]
245 fn test_parse_priority_label_known_slugs() {
246 assert_eq!(parse_priority_label("skip"), Some(FilePriority::Skip));
247 assert_eq!(parse_priority_label("low"), Some(FilePriority::Low));
248 assert_eq!(parse_priority_label("normal"), Some(FilePriority::Normal));
249 assert_eq!(parse_priority_label("high"), Some(FilePriority::High));
250 }
251
252 #[test]
253 fn test_parse_priority_label_unknown_returns_none() {
254 assert_eq!(parse_priority_label(""), None);
255 assert_eq!(parse_priority_label("SKIP"), None);
256 assert_eq!(parse_priority_label("medium"), None);
257 assert_eq!(parse_priority_label("4"), None);
258 }
259
260 #[test]
261 fn test_format_eta() {
262 assert_eq!(format_eta(0, 1), "0s");
264 assert_eq!(format_eta(1_000_000, 0), "\u{2014}");
266 assert_eq!(format_eta(30, 1), "30s");
268 assert_eq!(format_eta(45 * 60 + 12, 1), "45m 12s");
270 assert_eq!(format_eta(2 * 3600 + 15 * 60, 1), "2h 15m");
272 assert_eq!(format_eta(2 * 86400 + 15 * 3600, 1), "2d 15h");
274 assert_eq!(format_eta(100 * 1_048_576, 1_048_576), "1m 40s");
276 }
277
278 #[test]
279 fn format_state_with_super_seeding_shows_super_seeding() {
280 assert_eq!(
281 format_state_with_super_seeding(&TorrentState::Seeding, false, true),
282 "super seeding"
283 );
284 }
285
286 #[test]
287 fn format_state_with_super_seeding_normal_seeding() {
288 assert_eq!(
289 format_state_with_super_seeding(&TorrentState::Seeding, false, false),
290 "seeding"
291 );
292 }
293
294 #[test]
295 fn format_state_with_super_seeding_non_seeding_state() {
296 assert_eq!(
297 format_state_with_super_seeding(&TorrentState::Downloading, false, true),
298 "downloading"
299 );
300 }
301}