Skip to main content

irontide_dht/
compact.rs

1use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
2
3use irontide_core::Id20;
4
5use crate::error::{Error, Result};
6
7/// A DHT node: 20-byte ID + IPv4 socket address.
8///
9/// Encoded as 26 bytes: 20-byte node ID, 4-byte IPv4, 2-byte port (big-endian).
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct CompactNodeInfo {
12    /// 20-byte node ID.
13    pub id: Id20,
14    /// IPv4 socket address (IP + port).
15    pub addr: SocketAddr,
16}
17
18/// Size of one compact node info entry.
19pub const COMPACT_NODE_SIZE: usize = 26;
20
21impl CompactNodeInfo {
22    /// Encode to 26 bytes.
23    #[must_use]
24    pub fn to_bytes(&self) -> [u8; COMPACT_NODE_SIZE] {
25        let mut buf = [0u8; COMPACT_NODE_SIZE];
26        buf[..20].copy_from_slice(self.id.as_bytes());
27        match self.addr {
28            SocketAddr::V4(v4) => {
29                buf[20..24].copy_from_slice(&v4.ip().octets());
30                buf[24..26].copy_from_slice(&v4.port().to_be_bytes());
31            }
32            SocketAddr::V6(_) => {
33                // BEP 5 specifies IPv4 only; IPv6 nodes silently encode as 0.0.0.0:0
34            }
35        }
36        buf
37    }
38
39    /// Decode from exactly 26 bytes.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if `data` is not exactly 26 bytes.
44    pub fn from_bytes(data: &[u8]) -> Result<Self> {
45        if data.len() != COMPACT_NODE_SIZE {
46            return Err(Error::InvalidCompactNode(format!(
47                "expected {COMPACT_NODE_SIZE} bytes, got {}",
48                data.len()
49            )));
50        }
51        let id =
52            Id20::from_bytes(&data[..20]).map_err(|e| Error::InvalidCompactNode(e.to_string()))?;
53        let ip = Ipv4Addr::new(data[20], data[21], data[22], data[23]);
54        let port = u16::from_be_bytes([data[24], data[25]]);
55        Ok(Self {
56            id,
57            addr: SocketAddr::V4(SocketAddrV4::new(ip, port)),
58        })
59    }
60}
61
62/// Decode a byte slice of concatenated 26-byte compact node infos.
63///
64/// # Errors
65///
66/// Returns an error if the data length is not a multiple of 26.
67pub fn parse_compact_nodes(data: &[u8]) -> Result<Vec<CompactNodeInfo>> {
68    if !data.len().is_multiple_of(COMPACT_NODE_SIZE) {
69        return Err(Error::InvalidCompactNode(format!(
70            "compact nodes length {} is not a multiple of {COMPACT_NODE_SIZE}",
71            data.len()
72        )));
73    }
74    let mut nodes = Vec::with_capacity(data.len() / COMPACT_NODE_SIZE);
75    for chunk in data.chunks_exact(COMPACT_NODE_SIZE) {
76        nodes.push(CompactNodeInfo::from_bytes(chunk)?);
77    }
78    Ok(nodes)
79}
80
81/// Encode a slice of compact node infos into bytes.
82#[must_use]
83pub fn encode_compact_nodes(nodes: &[CompactNodeInfo]) -> Vec<u8> {
84    let mut buf = Vec::with_capacity(nodes.len() * COMPACT_NODE_SIZE);
85    for node in nodes {
86        buf.extend_from_slice(&node.to_bytes());
87    }
88    buf
89}
90
91/// A DHT node: 20-byte ID + IPv6 socket address.
92///
93/// Encoded as 38 bytes: 20-byte node ID, 16-byte IPv6, 2-byte port (big-endian).
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct CompactNodeInfo6 {
96    /// 20-byte node ID.
97    pub id: Id20,
98    /// IPv6 socket address (IP + port).
99    pub addr: SocketAddr,
100}
101
102/// Size of one compact IPv6 node info entry.
103pub const COMPACT_NODE6_SIZE: usize = 38;
104
105impl CompactNodeInfo6 {
106    /// Encode to 38 bytes.
107    #[must_use]
108    pub fn to_bytes(&self) -> [u8; COMPACT_NODE6_SIZE] {
109        let mut buf = [0u8; COMPACT_NODE6_SIZE];
110        buf[..20].copy_from_slice(self.id.as_bytes());
111        match self.addr {
112            SocketAddr::V6(v6) => {
113                buf[20..36].copy_from_slice(&v6.ip().octets());
114                buf[36..38].copy_from_slice(&v6.port().to_be_bytes());
115            }
116            SocketAddr::V4(_) => {
117                // BEP 24 specifies IPv6 only; IPv4 nodes silently encode as [::]:0
118            }
119        }
120        buf
121    }
122
123    /// Decode from exactly 38 bytes.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if `data` is not exactly 38 bytes.
128    pub fn from_bytes(data: &[u8]) -> Result<Self> {
129        if data.len() != COMPACT_NODE6_SIZE {
130            return Err(Error::InvalidCompactNode(format!(
131                "expected {COMPACT_NODE6_SIZE} bytes, got {}",
132                data.len()
133            )));
134        }
135        let id =
136            Id20::from_bytes(&data[..20]).map_err(|e| Error::InvalidCompactNode(e.to_string()))?;
137        let ip = Ipv6Addr::from(<[u8; 16]>::try_from(&data[20..36]).unwrap());
138        let port = u16::from_be_bytes([data[36], data[37]]);
139        Ok(Self {
140            id,
141            addr: SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)),
142        })
143    }
144}
145
146/// Decode a byte slice of concatenated 38-byte compact IPv6 node infos.
147///
148/// # Errors
149///
150/// Returns an error if the data length is not a multiple of 38.
151pub fn parse_compact_nodes6(data: &[u8]) -> Result<Vec<CompactNodeInfo6>> {
152    if !data.len().is_multiple_of(COMPACT_NODE6_SIZE) {
153        return Err(Error::InvalidCompactNode(format!(
154            "compact nodes6 length {} is not a multiple of {COMPACT_NODE6_SIZE}",
155            data.len()
156        )));
157    }
158    let mut nodes = Vec::with_capacity(data.len() / COMPACT_NODE6_SIZE);
159    for chunk in data.chunks_exact(COMPACT_NODE6_SIZE) {
160        nodes.push(CompactNodeInfo6::from_bytes(chunk)?);
161    }
162    Ok(nodes)
163}
164
165/// Encode a slice of compact IPv6 node infos into bytes.
166#[must_use]
167pub fn encode_compact_nodes6(nodes: &[CompactNodeInfo6]) -> Vec<u8> {
168    let mut buf = Vec::with_capacity(nodes.len() * COMPACT_NODE6_SIZE);
169    for node in nodes {
170        buf.extend_from_slice(&node.to_bytes());
171    }
172    buf
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    fn sample_node() -> CompactNodeInfo {
180        CompactNodeInfo {
181            id: Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
182            addr: "192.168.1.1:6881".parse().unwrap(),
183        }
184    }
185
186    #[test]
187    fn round_trip_single() {
188        let node = sample_node();
189        let bytes = node.to_bytes();
190        assert_eq!(bytes.len(), 26);
191        let decoded = CompactNodeInfo::from_bytes(&bytes).unwrap();
192        assert_eq!(node, decoded);
193    }
194
195    #[test]
196    fn round_trip_multiple() {
197        let nodes = vec![
198            sample_node(),
199            CompactNodeInfo {
200                id: Id20::ZERO,
201                addr: "10.0.0.1:8080".parse().unwrap(),
202            },
203        ];
204        let encoded = encode_compact_nodes(&nodes);
205        assert_eq!(encoded.len(), 52);
206        let decoded = parse_compact_nodes(&encoded).unwrap();
207        assert_eq!(nodes, decoded);
208    }
209
210    #[test]
211    fn boundary_ip_and_port() {
212        let node = CompactNodeInfo {
213            id: Id20::from_hex("ffffffffffffffffffffffffffffffffffffffff").unwrap(),
214            addr: "255.255.255.255:65535".parse().unwrap(),
215        };
216        let bytes = node.to_bytes();
217        let decoded = CompactNodeInfo::from_bytes(&bytes).unwrap();
218        assert_eq!(node, decoded);
219    }
220
221    #[test]
222    fn reject_wrong_length() {
223        assert!(CompactNodeInfo::from_bytes(&[0u8; 25]).is_err());
224        assert!(parse_compact_nodes(&[0u8; 27]).is_err());
225    }
226
227    #[test]
228    fn empty_compact_nodes() {
229        let nodes = parse_compact_nodes(&[]).unwrap();
230        assert!(nodes.is_empty());
231    }
232
233    // --- CompactNodeInfo6 (IPv6) ---
234
235    fn sample_node6() -> CompactNodeInfo6 {
236        CompactNodeInfo6 {
237            id: Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap(),
238            addr: "[2001:db8::1]:6881".parse().unwrap(),
239        }
240    }
241
242    #[test]
243    fn round_trip_single_v6() {
244        let node = sample_node6();
245        let bytes = node.to_bytes();
246        assert_eq!(bytes.len(), 38);
247        let decoded = CompactNodeInfo6::from_bytes(&bytes).unwrap();
248        assert_eq!(node, decoded);
249    }
250
251    #[test]
252    fn round_trip_multiple_v6() {
253        let nodes = vec![
254            sample_node6(),
255            CompactNodeInfo6 {
256                id: Id20::ZERO,
257                addr: "[::1]:8080".parse().unwrap(),
258            },
259        ];
260        let encoded = encode_compact_nodes6(&nodes);
261        assert_eq!(encoded.len(), 76);
262        let decoded = parse_compact_nodes6(&encoded).unwrap();
263        assert_eq!(nodes, decoded);
264    }
265
266    #[test]
267    fn boundary_v6() {
268        let node = CompactNodeInfo6 {
269            id: Id20::from_hex("ffffffffffffffffffffffffffffffffffffffff").unwrap(),
270            addr: "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:65535"
271                .parse()
272                .unwrap(),
273        };
274        let bytes = node.to_bytes();
275        let decoded = CompactNodeInfo6::from_bytes(&bytes).unwrap();
276        assert_eq!(node, decoded);
277    }
278
279    #[test]
280    fn reject_wrong_length_v6() {
281        assert!(CompactNodeInfo6::from_bytes(&[0u8; 37]).is_err());
282        assert!(parse_compact_nodes6(&[0u8; 39]).is_err());
283    }
284
285    #[test]
286    fn empty_compact_nodes6() {
287        let nodes = parse_compact_nodes6(&[]).unwrap();
288        assert!(nodes.is_empty());
289    }
290}