ipfrs_network/
utils.rs

1//! Network Utilities
2//!
3//! This module provides common utility functions for network operations.
4
5use libp2p::{Multiaddr, PeerId};
6use std::time::Duration;
7
8/// Format bytes in human-readable format (B, KB, MB, GB, TB)
9///
10/// # Examples
11///
12/// ```
13/// use ipfrs_network::utils::format_bytes;
14///
15/// assert_eq!(format_bytes(1024), "1.00 KB");
16/// assert_eq!(format_bytes(1_048_576), "1.00 MB");
17/// assert_eq!(format_bytes(500), "500 B");
18/// ```
19pub fn format_bytes(bytes: usize) -> String {
20    const KB: usize = 1024;
21    const MB: usize = KB * 1024;
22    const GB: usize = MB * 1024;
23    const TB: usize = GB * 1024;
24
25    if bytes >= TB {
26        format!("{:.2} TB", bytes as f64 / TB as f64)
27    } else if bytes >= GB {
28        format!("{:.2} GB", bytes as f64 / GB as f64)
29    } else if bytes >= MB {
30        format!("{:.2} MB", bytes as f64 / MB as f64)
31    } else if bytes >= KB {
32        format!("{:.2} KB", bytes as f64 / KB as f64)
33    } else {
34        format!("{} B", bytes)
35    }
36}
37
38/// Format bytes per second in human-readable format (B/s, KB/s, MB/s, GB/s)
39///
40/// # Examples
41///
42/// ```
43/// use ipfrs_network::utils::format_bandwidth;
44///
45/// assert_eq!(format_bandwidth(1024), "1.00 KB/s");
46/// assert_eq!(format_bandwidth(1_048_576), "1.00 MB/s");
47/// ```
48pub fn format_bandwidth(bytes_per_sec: usize) -> String {
49    format!("{}/s", format_bytes(bytes_per_sec))
50}
51
52/// Format duration in human-readable format
53///
54/// # Examples
55///
56/// ```
57/// use std::time::Duration;
58/// use ipfrs_network::utils::format_duration;
59///
60/// assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
61/// assert_eq!(format_duration(Duration::from_secs(3665)), "1h 1m 5s");
62/// assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
63/// ```
64pub fn format_duration(duration: Duration) -> String {
65    let total_secs = duration.as_secs();
66    let millis = duration.subsec_millis();
67
68    if total_secs == 0 {
69        if millis == 0 {
70            return format!("{}µs", duration.subsec_micros());
71        }
72        return format!("{}ms", millis);
73    }
74
75    let hours = total_secs / 3600;
76    let minutes = (total_secs % 3600) / 60;
77    let seconds = total_secs % 60;
78
79    let mut parts = Vec::new();
80    if hours > 0 {
81        parts.push(format!("{}h", hours));
82    }
83    if minutes > 0 {
84        parts.push(format!("{}m", minutes));
85    }
86    if seconds > 0 || parts.is_empty() {
87        parts.push(format!("{}s", seconds));
88    }
89
90    parts.join(" ")
91}
92
93/// Parse a multiaddress string
94///
95/// # Errors
96///
97/// Returns an error if the address cannot be parsed
98///
99/// # Examples
100///
101/// ```
102/// use ipfrs_network::utils::parse_multiaddr;
103///
104/// let addr = parse_multiaddr("/ip4/127.0.0.1/tcp/4001").unwrap();
105/// ```
106pub fn parse_multiaddr(addr: &str) -> Result<Multiaddr, String> {
107    addr.parse::<Multiaddr>()
108        .map_err(|e| format!("Failed to parse multiaddress: {}", e))
109}
110
111/// Parse multiple multiaddress strings
112///
113/// # Errors
114///
115/// Returns an error if any address cannot be parsed
116///
117/// # Examples
118///
119/// ```
120/// use ipfrs_network::utils::parse_multiaddrs;
121///
122/// let addrs = parse_multiaddrs(&[
123///     "/ip4/127.0.0.1/tcp/4001".to_string(),
124///     "/ip6/::1/tcp/4001".to_string(),
125/// ]).unwrap();
126/// assert_eq!(addrs.len(), 2);
127/// ```
128pub fn parse_multiaddrs(addrs: &[String]) -> Result<Vec<Multiaddr>, String> {
129    addrs.iter().map(|s| parse_multiaddr(s)).collect()
130}
131
132/// Check if a multiaddress is a local address (loopback or link-local)
133///
134/// # Examples
135///
136/// ```
137/// use ipfrs_network::utils::{parse_multiaddr, is_local_addr};
138///
139/// let local = parse_multiaddr("/ip4/127.0.0.1/tcp/4001").unwrap();
140/// assert!(is_local_addr(&local));
141///
142/// let public = parse_multiaddr("/ip4/8.8.8.8/tcp/4001").unwrap();
143/// assert!(!is_local_addr(&public));
144/// ```
145pub fn is_local_addr(addr: &Multiaddr) -> bool {
146    use libp2p::multiaddr::Protocol;
147
148    for proto in addr.iter() {
149        match proto {
150            Protocol::Ip4(ip) => {
151                return ip.is_loopback() || ip.is_link_local() || ip.is_private();
152            }
153            Protocol::Ip6(ip) => {
154                return ip.is_loopback() || ip.is_unicast_link_local();
155            }
156            _ => continue,
157        }
158    }
159    false
160}
161
162/// Check if a multiaddress is a public address
163///
164/// # Examples
165///
166/// ```
167/// use ipfrs_network::utils::{parse_multiaddr, is_public_addr};
168///
169/// let public = parse_multiaddr("/ip4/8.8.8.8/tcp/4001").unwrap();
170/// assert!(is_public_addr(&public));
171///
172/// let local = parse_multiaddr("/ip4/127.0.0.1/tcp/4001").unwrap();
173/// assert!(!is_public_addr(&local));
174/// ```
175pub fn is_public_addr(addr: &Multiaddr) -> bool {
176    !is_local_addr(addr)
177}
178
179/// Calculate exponential backoff duration
180///
181/// # Examples
182///
183/// ```
184/// use std::time::Duration;
185/// use ipfrs_network::utils::exponential_backoff;
186///
187/// assert_eq!(exponential_backoff(0, Duration::from_secs(1), Duration::from_secs(60)),
188///            Duration::from_secs(1));
189/// assert_eq!(exponential_backoff(1, Duration::from_secs(1), Duration::from_secs(60)),
190///            Duration::from_secs(2));
191/// assert_eq!(exponential_backoff(2, Duration::from_secs(1), Duration::from_secs(60)),
192///            Duration::from_secs(4));
193/// ```
194pub fn exponential_backoff(attempt: u32, base: Duration, max: Duration) -> Duration {
195    let backoff = base.saturating_mul(2_u32.saturating_pow(attempt));
196    backoff.min(max)
197}
198
199/// Calculate jittered exponential backoff duration
200///
201/// Adds random jitter (±25%) to prevent thundering herd problem
202///
203/// # Examples
204///
205/// ```
206/// use std::time::Duration;
207/// use ipfrs_network::utils::jittered_backoff;
208///
209/// let backoff = jittered_backoff(2, Duration::from_secs(1), Duration::from_secs(60));
210/// // Should be roughly 4 seconds ± 25%
211/// assert!(backoff >= Duration::from_secs(3));
212/// assert!(backoff <= Duration::from_secs(5));
213/// ```
214pub fn jittered_backoff(attempt: u32, base: Duration, max: Duration) -> Duration {
215    use rand::RngCore;
216    let backoff = exponential_backoff(attempt, base, max);
217    let mut rng = rand::rng();
218    let random_value = rng.next_u64() as f64 / u64::MAX as f64;
219    let jitter = 0.75 + (random_value * 0.5); // Maps [0, 1] to [0.75, 1.25]
220    Duration::from_secs_f64(backoff.as_secs_f64() * jitter)
221}
222
223/// Truncate a peer ID for display purposes
224///
225/// # Examples
226///
227/// ```
228/// use libp2p::PeerId;
229/// use ipfrs_network::utils::truncate_peer_id;
230///
231/// let peer_id = PeerId::random();
232/// let truncated = truncate_peer_id(&peer_id, 8);
233/// assert_eq!(truncated.len(), 11); // "12..." + 8 chars
234/// ```
235pub fn truncate_peer_id(peer_id: &PeerId, length: usize) -> String {
236    let s = peer_id.to_string();
237    if s.len() <= length + 3 {
238        s
239    } else {
240        format!("{}...{}", &s[..length / 2], &s[s.len() - length / 2..])
241    }
242}
243
244/// Calculate percentage with proper rounding
245///
246/// # Examples
247///
248/// ```
249/// use ipfrs_network::utils::percentage;
250///
251/// assert_eq!(percentage(25, 100), 25.0);
252/// assert_eq!(percentage(1, 3), 33.33);
253/// assert_eq!(percentage(0, 0), 0.0); // Handles division by zero
254/// ```
255pub fn percentage(value: usize, total: usize) -> f64 {
256    if total == 0 {
257        0.0
258    } else {
259        ((value as f64 / total as f64) * 10000.0).round() / 100.0
260    }
261}
262
263/// Calculate moving average
264///
265/// # Examples
266///
267/// ```
268/// use ipfrs_network::utils::moving_average;
269///
270/// let current = 10.0;
271/// let new_value = 20.0;
272/// let alpha = 0.5;
273///
274/// assert_eq!(moving_average(current, new_value, alpha), 15.0);
275/// ```
276pub fn moving_average(current: f64, new_value: f64, alpha: f64) -> f64 {
277    alpha * new_value + (1.0 - alpha) * current
278}
279
280/// Validate alpha value for exponential moving average
281///
282/// # Panics
283///
284/// Panics if alpha is not in range [0.0, 1.0]
285///
286/// # Examples
287///
288/// ```
289/// use ipfrs_network::utils::validate_alpha;
290///
291/// validate_alpha(0.5); // OK
292/// validate_alpha(0.0); // OK
293/// validate_alpha(1.0); // OK
294/// ```
295///
296/// ```should_panic
297/// use ipfrs_network::utils::validate_alpha;
298///
299/// validate_alpha(1.5); // Panics
300/// ```
301pub fn validate_alpha(alpha: f64) {
302    assert!(
303        (0.0..=1.0).contains(&alpha),
304        "Alpha must be in range [0.0, 1.0], got {}",
305        alpha
306    );
307}
308
309/// Check if two peer IDs match
310///
311/// # Examples
312///
313/// ```
314/// use libp2p::PeerId;
315/// use ipfrs_network::utils::peers_match;
316///
317/// let peer1 = PeerId::random();
318/// let peer2 = peer1;
319/// let peer3 = PeerId::random();
320///
321/// assert!(peers_match(&peer1, &peer2));
322/// assert!(!peers_match(&peer1, &peer3));
323/// ```
324pub fn peers_match(peer1: &PeerId, peer2: &PeerId) -> bool {
325    peer1 == peer2
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_format_bytes() {
334        assert_eq!(format_bytes(0), "0 B");
335        assert_eq!(format_bytes(500), "500 B");
336        assert_eq!(format_bytes(1024), "1.00 KB");
337        assert_eq!(format_bytes(1_048_576), "1.00 MB");
338        assert_eq!(format_bytes(1_073_741_824), "1.00 GB");
339        assert_eq!(format_bytes(1_099_511_627_776), "1.00 TB");
340    }
341
342    #[test]
343    fn test_format_bandwidth() {
344        assert_eq!(format_bandwidth(1024), "1.00 KB/s");
345        assert_eq!(format_bandwidth(1_048_576), "1.00 MB/s");
346    }
347
348    #[test]
349    fn test_format_duration() {
350        assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
351        assert_eq!(format_duration(Duration::from_secs(30)), "30s");
352        assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
353        assert_eq!(format_duration(Duration::from_secs(3665)), "1h 1m 5s");
354        assert_eq!(format_duration(Duration::from_secs(7200)), "2h");
355    }
356
357    #[test]
358    fn test_parse_multiaddr() {
359        let addr = parse_multiaddr("/ip4/127.0.0.1/tcp/4001").unwrap();
360        assert!(addr.to_string().contains("127.0.0.1"));
361    }
362
363    #[test]
364    fn test_parse_multiaddrs() {
365        let addrs = parse_multiaddrs(&[
366            "/ip4/127.0.0.1/tcp/4001".to_string(),
367            "/ip6/::1/tcp/4001".to_string(),
368        ])
369        .unwrap();
370        assert_eq!(addrs.len(), 2);
371    }
372
373    #[test]
374    fn test_is_local_addr() {
375        let local = parse_multiaddr("/ip4/127.0.0.1/tcp/4001").unwrap();
376        assert!(is_local_addr(&local));
377
378        let local_ipv6 = parse_multiaddr("/ip6/::1/tcp/4001").unwrap();
379        assert!(is_local_addr(&local_ipv6));
380
381        let private = parse_multiaddr("/ip4/192.168.1.1/tcp/4001").unwrap();
382        assert!(is_local_addr(&private));
383
384        let public = parse_multiaddr("/ip4/8.8.8.8/tcp/4001").unwrap();
385        assert!(!is_local_addr(&public));
386    }
387
388    #[test]
389    fn test_is_public_addr() {
390        let public = parse_multiaddr("/ip4/8.8.8.8/tcp/4001").unwrap();
391        assert!(is_public_addr(&public));
392
393        let local = parse_multiaddr("/ip4/127.0.0.1/tcp/4001").unwrap();
394        assert!(!is_public_addr(&local));
395    }
396
397    #[test]
398    fn test_exponential_backoff() {
399        let base = Duration::from_secs(1);
400        let max = Duration::from_secs(60);
401
402        assert_eq!(exponential_backoff(0, base, max), Duration::from_secs(1));
403        assert_eq!(exponential_backoff(1, base, max), Duration::from_secs(2));
404        assert_eq!(exponential_backoff(2, base, max), Duration::from_secs(4));
405        assert_eq!(exponential_backoff(3, base, max), Duration::from_secs(8));
406        assert_eq!(exponential_backoff(10, base, max), Duration::from_secs(60));
407        // Capped at max
408    }
409
410    #[test]
411    fn test_jittered_backoff() {
412        let base = Duration::from_secs(1);
413        let max = Duration::from_secs(60);
414
415        for attempt in 0..5 {
416            let backoff = jittered_backoff(attempt, base, max);
417            let expected = exponential_backoff(attempt, base, max);
418            // Jitter should be within ±25%
419            assert!(backoff.as_secs_f64() >= expected.as_secs_f64() * 0.75);
420            assert!(backoff.as_secs_f64() <= expected.as_secs_f64() * 1.25);
421        }
422    }
423
424    #[test]
425    fn test_truncate_peer_id() {
426        let peer_id = PeerId::random();
427        let truncated = truncate_peer_id(&peer_id, 8);
428        assert!(truncated.len() <= peer_id.to_string().len());
429        assert!(truncated.contains("..."));
430    }
431
432    #[test]
433    fn test_percentage() {
434        assert_eq!(percentage(25, 100), 25.0);
435        assert_eq!(percentage(1, 3), 33.33);
436        assert_eq!(percentage(2, 3), 66.67);
437        assert_eq!(percentage(0, 0), 0.0);
438        assert_eq!(percentage(5, 0), 0.0);
439    }
440
441    #[test]
442    fn test_moving_average() {
443        assert_eq!(moving_average(10.0, 20.0, 0.5), 15.0);
444        assert_eq!(moving_average(10.0, 20.0, 0.0), 10.0);
445        assert_eq!(moving_average(10.0, 20.0, 1.0), 20.0);
446    }
447
448    #[test]
449    fn test_validate_alpha() {
450        validate_alpha(0.0);
451        validate_alpha(0.5);
452        validate_alpha(1.0);
453    }
454
455    #[test]
456    #[should_panic(expected = "Alpha must be in range")]
457    fn test_validate_alpha_too_high() {
458        validate_alpha(1.5);
459    }
460
461    #[test]
462    #[should_panic(expected = "Alpha must be in range")]
463    fn test_validate_alpha_negative() {
464        validate_alpha(-0.1);
465    }
466
467    #[test]
468    fn test_peers_match() {
469        let peer1 = PeerId::random();
470        let peer2 = peer1;
471        let peer3 = PeerId::random();
472
473        assert!(peers_match(&peer1, &peer2));
474        assert!(!peers_match(&peer1, &peer3));
475    }
476}