chie_shared/utils/
network.rs

1//! Network and peer utility functions.
2
3/// Extract the peer ID from a libp2p multiaddress string.
4/// Example: "/ip4/127.0.0.1/tcp/4001/p2p/QmPeerId" -> "QmPeerId"
5///
6/// # Examples
7///
8/// ```
9/// use chie_shared::extract_peer_id_from_multiaddr;
10///
11/// // Extract peer ID from multiaddr with p2p protocol
12/// let addr = "/ip4/127.0.0.1/tcp/4001/p2p/QmPeerId123";
13/// assert_eq!(
14///     extract_peer_id_from_multiaddr(addr),
15///     Some("QmPeerId123".to_string())
16/// );
17///
18/// // Extract peer ID from multiaddr with ipfs protocol
19/// let addr = "/ip4/192.168.1.1/tcp/4001/ipfs/12D3KooTest";
20/// assert_eq!(
21///     extract_peer_id_from_multiaddr(addr),
22///     Some("12D3KooTest".to_string())
23/// );
24///
25/// // No peer ID in multiaddr
26/// let addr = "/ip4/127.0.0.1/tcp/4001";
27/// assert_eq!(extract_peer_id_from_multiaddr(addr), None);
28/// ```
29#[must_use]
30pub fn extract_peer_id_from_multiaddr(multiaddr: &str) -> Option<String> {
31    multiaddr
32        .split('/')
33        .skip_while(|&s| s != "p2p" && s != "ipfs")
34        .nth(1)
35        .map(ToString::to_string)
36}
37
38/// Validate peer ID format (basic check for IPFS/libp2p peer IDs).
39/// Checks if it starts with common prefixes and has reasonable length.
40#[must_use]
41pub fn is_valid_peer_id_format(peer_id: &str) -> bool {
42    if peer_id.is_empty() {
43        return false;
44    }
45
46    // Common peer ID prefixes
47    let valid_prefix = peer_id.starts_with("Qm")  // CIDv0
48        || peer_id.starts_with("12D3")            // CIDv1 base58btc
49        || peer_id.starts_with("bafz"); // CIDv1 base32
50
51    // Reasonable length (peer IDs are typically 40-60 characters)
52    let valid_length = (30..=100).contains(&peer_id.len());
53
54    valid_prefix && valid_length && peer_id.chars().all(|c| c.is_alphanumeric())
55}
56
57/// Parse bandwidth from string (e.g., "100 Mbps", "1 Gbps").
58///
59/// # Examples
60///
61/// ```
62/// use chie_shared::parse_bandwidth_str;
63///
64/// // Parse different bandwidth units
65/// assert_eq!(parse_bandwidth_str("100 bps"), Some(100));
66/// assert_eq!(parse_bandwidth_str("10 Kbps"), Some(10_000));
67/// assert_eq!(parse_bandwidth_str("100 Mbps"), Some(100_000_000));
68/// assert_eq!(parse_bandwidth_str("1 Gbps"), Some(1_000_000_000));
69///
70/// // Case insensitive
71/// assert_eq!(parse_bandwidth_str("50 MBPS"), Some(50_000_000));
72///
73/// // Invalid formats
74/// assert_eq!(parse_bandwidth_str("invalid"), None);
75/// assert_eq!(parse_bandwidth_str("100"), None); // Missing unit
76/// ```
77pub fn parse_bandwidth_str(s: &str) -> Option<u64> {
78    let s = s.trim().to_lowercase();
79    let parts: Vec<&str> = s.split_whitespace().collect();
80
81    if parts.len() != 2 {
82        return None;
83    }
84
85    let value: f64 = parts[0].parse().ok()?;
86    let multiplier = match parts[1] {
87        "bps" => 1.0,
88        "kbps" => 1_000.0,
89        "mbps" => 1_000_000.0,
90        "gbps" => 1_000_000_000.0,
91        _ => return None,
92    };
93
94    Some((value * multiplier) as u64)
95}
96
97/// Generate a human-readable session ID.
98pub fn generate_session_id() -> String {
99    uuid::Uuid::new_v4().to_string()
100}
101
102/// Split a vector into chunks of specified size.
103/// Returns a vector of vectors, where each inner vector has at most `chunk_size` elements.
104pub fn chunk_vec<T: Clone>(items: &[T], chunk_size: usize) -> Vec<Vec<T>> {
105    if chunk_size == 0 {
106        return vec![];
107    }
108
109    items
110        .chunks(chunk_size)
111        .map(|chunk| chunk.to_vec())
112        .collect()
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_extract_peer_id_from_multiaddr() {
121        assert_eq!(
122            extract_peer_id_from_multiaddr("/ip4/127.0.0.1/tcp/4001/p2p/QmPeerId"),
123            Some("QmPeerId".to_string())
124        );
125        assert_eq!(
126            extract_peer_id_from_multiaddr("/ip4/192.168.1.1/tcp/4001/ipfs/QmTest123"),
127            Some("QmTest123".to_string())
128        );
129        assert_eq!(
130            extract_peer_id_from_multiaddr("/ip4/127.0.0.1/tcp/4001"),
131            None
132        );
133        assert_eq!(
134            extract_peer_id_from_multiaddr("/p2p/QmSimple"),
135            Some("QmSimple".to_string())
136        );
137    }
138
139    #[test]
140    fn test_is_valid_peer_id_format() {
141        // Valid CIDv0
142        assert!(is_valid_peer_id_format(
143            "QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N"
144        ));
145
146        // Valid CIDv1 base58btc
147        assert!(is_valid_peer_id_format(
148            "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
149        ));
150
151        // Valid CIDv1 base32
152        assert!(is_valid_peer_id_format(
153            "bafzbeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
154        ));
155
156        // Invalid - wrong prefix
157        assert!(!is_valid_peer_id_format(
158            "Xm1234567890123456789012345678901234567890"
159        ));
160
161        // Invalid - too short
162        assert!(!is_valid_peer_id_format("QmShort"));
163
164        // Invalid - empty
165        assert!(!is_valid_peer_id_format(""));
166
167        // Invalid - non-alphanumeric
168        assert!(!is_valid_peer_id_format(
169            "Qm12345678901234567890123456789012345678-invalid"
170        ));
171    }
172
173    #[test]
174    fn test_parse_bandwidth_str() {
175        assert_eq!(parse_bandwidth_str("100 bps"), Some(100));
176        assert_eq!(parse_bandwidth_str("10 Kbps"), Some(10_000));
177        assert_eq!(parse_bandwidth_str("100 Mbps"), Some(100_000_000));
178        assert_eq!(parse_bandwidth_str("1 Gbps"), Some(1_000_000_000));
179        assert_eq!(parse_bandwidth_str("invalid"), None);
180        assert_eq!(parse_bandwidth_str("100"), None);
181    }
182
183    #[test]
184    fn test_generate_session_id() {
185        let id1 = generate_session_id();
186        let id2 = generate_session_id();
187        assert_ne!(id1, id2);
188        assert!(uuid::Uuid::parse_str(&id1).is_ok());
189    }
190
191    #[test]
192    fn test_chunk_vec() {
193        let items = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
194        let chunks = chunk_vec(&items, 3);
195        assert_eq!(chunks.len(), 3);
196        assert_eq!(chunks[0], vec![1, 2, 3]);
197        assert_eq!(chunks[1], vec![4, 5, 6]);
198        assert_eq!(chunks[2], vec![7, 8, 9]);
199
200        let chunks = chunk_vec(&items, 4);
201        assert_eq!(chunks.len(), 3);
202        assert_eq!(chunks[0], vec![1, 2, 3, 4]);
203        assert_eq!(chunks[1], vec![5, 6, 7, 8]);
204        assert_eq!(chunks[2], vec![9]);
205
206        let chunks = chunk_vec(&items, 0);
207        assert_eq!(chunks.len(), 0);
208
209        let empty: Vec<i32> = vec![];
210        let chunks = chunk_vec(&empty, 3);
211        assert_eq!(chunks.len(), 0);
212    }
213}