Skip to main content

irontide_dht/
krpc.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_possible_wrap,
4    clippy::cast_sign_loss,
5    reason = "M175: BEP 5 KRPC — port/transaction-id field widths fixed by spec"
6)]
7
8//! KRPC message encoding and decoding (BEP 5).
9//!
10//! KRPC messages are bencoded dictionaries with keys:
11//! - `t` — transaction ID (binary string, 2 bytes)
12//! - `y` — message type: `q` (query), `r` (response), `e` (error)
13//! - `q` — query method name (for queries only)
14//! - `a` — query arguments dict (for queries only)
15//! - `r` — response values dict (for responses only)
16//! - `e` — error list `[code, message]` (for errors only)
17
18use std::collections::BTreeMap;
19
20use irontide_bencode::{self as bencode, BencodeValue};
21use irontide_core::Id20;
22
23use crate::compact::{
24    CompactNodeInfo, CompactNodeInfo6, encode_compact_nodes, encode_compact_nodes6,
25    parse_compact_nodes, parse_compact_nodes6,
26};
27use crate::error::{Error, Result};
28
29/// 2-byte transaction ID for matching requests to responses.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct TransactionId(pub [u8; 2]);
32
33impl TransactionId {
34    /// Create a transaction ID from a 16-bit integer.
35    #[must_use]
36    pub fn from_u16(val: u16) -> Self {
37        Self(val.to_be_bytes())
38    }
39
40    /// Return the transaction ID as a 16-bit integer.
41    #[must_use]
42    pub fn as_u16(&self) -> u16 {
43        u16::from_be_bytes(self.0)
44    }
45
46    /// Parse a transaction ID from raw bytes (pads 1-byte IDs with zero).
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if `bytes` is empty.
51    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
52        if bytes.len() < 2 {
53            // Some implementations use 1-byte transaction IDs; pad with zero
54            let mut buf = [0u8; 2];
55            buf[..bytes.len()].copy_from_slice(bytes);
56            return Ok(Self(buf));
57        }
58        Ok(Self([bytes[0], bytes[1]]))
59    }
60}
61
62/// A parsed KRPC message.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct KrpcMessage {
65    /// Opaque 2-byte identifier matching requests to responses.
66    pub transaction_id: TransactionId,
67    /// Message payload (query, response, or error).
68    pub body: KrpcBody,
69    /// BEP 42: Compact IP+port of the message recipient, included in responses.
70    pub sender_ip: Option<std::net::SocketAddr>,
71    /// BEP 43: Read-only node flag. When true, the sending node should not be
72    /// added to the routing table (it cannot store data or respond to queries
73    /// from arbitrary nodes).
74    pub read_only: bool,
75}
76
77/// The body of a KRPC message.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum KrpcBody {
80    /// A query from a remote node.
81    Query(KrpcQuery),
82    /// A response to one of our queries.
83    Response(KrpcResponse),
84    /// An error response.
85    Error {
86        /// KRPC error code.
87        code: i64,
88        /// Human-readable error description.
89        message: String,
90    },
91}
92
93/// BEP 45: Address family requested in the `want` field of queries.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub enum WantFamily {
96    /// IPv4 nodes (`"n4"` on the wire).
97    N4,
98    /// IPv6 nodes (`"n6"` on the wire).
99    N6,
100}
101
102impl WantFamily {
103    /// Wire representation as a bencode byte string.
104    #[must_use]
105    pub fn as_bytes(&self) -> &'static [u8] {
106        match self {
107            Self::N4 => b"n4",
108            Self::N6 => b"n6",
109        }
110    }
111
112    /// Parse from wire bytes; returns `None` for unrecognised values.
113    #[must_use]
114    pub fn from_bytes(b: &[u8]) -> Option<Self> {
115        match b {
116            b"n4" => Some(Self::N4),
117            b"n6" => Some(Self::N6),
118            _ => None,
119        }
120    }
121}
122
123/// KRPC query types (BEP 5).
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum KrpcQuery {
126    /// Liveness check.
127    Ping {
128        /// Querying node's ID.
129        id: Id20,
130    },
131    /// Find the closest nodes to a target ID.
132    FindNode {
133        /// Querying node's ID.
134        id: Id20,
135        /// Target node ID to search for.
136        target: Id20,
137        /// BEP 45: requested address families.
138        want: Option<Vec<WantFamily>>,
139    },
140    /// Find peers downloading a torrent.
141    GetPeers {
142        /// Querying node's ID.
143        id: Id20,
144        /// Torrent info hash to search for.
145        info_hash: Id20,
146        /// BEP 33: if set to 1, exclude seed peers from results.
147        noseed: Option<i64>,
148        /// BEP 33: if set to 1, include bloom filter scrape data in response.
149        scrape: Option<i64>,
150        /// BEP 45: requested address families.
151        want: Option<Vec<WantFamily>>,
152    },
153    /// Announce that we are downloading a torrent.
154    AnnouncePeer {
155        /// Querying node's ID.
156        id: Id20,
157        /// Torrent info hash being announced.
158        info_hash: Id20,
159        /// Port we are listening on.
160        port: u16,
161        /// If true, use the UDP source port instead of the `port` field.
162        implied_port: bool,
163        /// Write token obtained from a prior `get_peers` response.
164        token: Vec<u8>,
165    },
166    /// BEP 44: get an item from DHT storage.
167    Get {
168        /// Querying node's ID.
169        id: Id20,
170        /// Target hash: SHA-1(value) for immutable, SHA-1(pubkey+salt) for mutable.
171        target: Id20,
172        /// Optional: if set, only return mutable items with seq > this value.
173        seq: Option<i64>,
174    },
175    /// BEP 44: put an item into DHT storage.
176    Put {
177        /// Querying node's ID.
178        id: Id20,
179        /// Write token (obtained from a prior get response).
180        token: Vec<u8>,
181        /// The bencoded value to store.
182        value: Vec<u8>,
183        /// For mutable items: ed25519 public key (32 bytes).
184        key: Option<[u8; 32]>,
185        /// For mutable items: ed25519 signature (64 bytes).
186        signature: Option<[u8; 64]>,
187        /// For mutable items: sequence number.
188        seq: Option<i64>,
189        /// For mutable items: optional salt.
190        salt: Option<Vec<u8>>,
191        /// For mutable items: optional CAS (compare-and-swap) expected seq.
192        cas: Option<i64>,
193    },
194    /// BEP 51: sample info hashes from a node's storage.
195    SampleInfohashes {
196        /// Querying node's ID.
197        id: Id20,
198        /// Target ID for DHT traversal.
199        target: Id20,
200    },
201}
202
203/// KRPC response types.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum KrpcResponse {
206    /// Response to ping or `announce_peer` — just the node ID.
207    NodeId {
208        /// Responding node's ID.
209        id: Id20,
210    },
211    /// Response to `find_node`.
212    FindNode {
213        /// Responding node's ID.
214        id: Id20,
215        /// Closest known IPv4 nodes.
216        nodes: Vec<CompactNodeInfo>,
217        /// Closest known IPv6 nodes (BEP 24).
218        nodes6: Vec<CompactNodeInfo6>,
219    },
220    /// Response to `get_peers` — either peers or closer nodes.
221    GetPeers(GetPeersResponse),
222    /// BEP 44: response to a get query.
223    GetItem {
224        /// Responding node's ID.
225        id: Id20,
226        /// Write token for subsequent put operations.
227        token: Option<Vec<u8>>,
228        /// Closest known IPv4 nodes.
229        nodes: Vec<CompactNodeInfo>,
230        /// Closest known IPv6 nodes.
231        nodes6: Vec<CompactNodeInfo6>,
232        /// The stored value (if found).
233        value: Option<Vec<u8>>,
234        /// For mutable items: ed25519 public key.
235        key: Option<[u8; 32]>,
236        /// For mutable items: signature.
237        signature: Option<[u8; 64]>,
238        /// For mutable items: sequence number.
239        seq: Option<i64>,
240    },
241    /// Response to `sample_infohashes` (BEP 51).
242    SampleInfohashes(SampleInfohashesResponse),
243}
244
245/// `get_peers` response can return peers, closer nodes, or both.
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct GetPeersResponse {
248    /// Responding node's ID.
249    pub id: Id20,
250    /// Write token for `announce_peer`.
251    pub token: Option<Vec<u8>>,
252    /// Direct peer addresses (compact: 6 bytes each for IPv4, 18 bytes for IPv6).
253    pub peers: Vec<std::net::SocketAddr>,
254    /// Closer nodes (compact: 26 bytes each).
255    pub nodes: Vec<CompactNodeInfo>,
256    /// Closer IPv6 nodes (BEP 24, compact: 38 bytes each).
257    pub nodes6: Vec<CompactNodeInfo6>,
258    /// BEP 33: peer/leecher bloom filter (256 bytes).
259    pub bfpe: Option<Vec<u8>>,
260    /// BEP 33: seed bloom filter (256 bytes).
261    pub bfsd: Option<Vec<u8>>,
262}
263
264/// Response to `sample_infohashes` (BEP 51).
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct SampleInfohashesResponse {
267    /// Responding node's ID.
268    pub id: Id20,
269    /// Minimum seconds before querying this node again.
270    pub interval: i64,
271    /// Estimated total number of info hashes in this node's storage.
272    pub num: i64,
273    /// Random sample of info hashes (each 20 bytes).
274    pub samples: Vec<Id20>,
275    /// Closer nodes (compact format), for DHT traversal.
276    pub nodes: Vec<CompactNodeInfo>,
277}
278
279impl KrpcQuery {
280    /// The query method name as used in the `q` field.
281    #[must_use]
282    pub fn method_name(&self) -> &'static str {
283        match self {
284            Self::Ping { .. } => "ping",
285            Self::FindNode { .. } => "find_node",
286            Self::GetPeers { .. } => "get_peers",
287            Self::AnnouncePeer { .. } => "announce_peer",
288            Self::Get { .. } => "get",
289            Self::Put { .. } => "put",
290            Self::SampleInfohashes { .. } => "sample_infohashes",
291        }
292    }
293
294    /// The querying node's ID.
295    #[must_use]
296    pub fn sender_id(&self) -> &Id20 {
297        match self {
298            Self::Ping { id }
299            | Self::FindNode { id, .. }
300            | Self::GetPeers { id, .. }
301            | Self::AnnouncePeer { id, .. }
302            | Self::Get { id, .. }
303            | Self::Put { id, .. }
304            | Self::SampleInfohashes { id, .. } => id,
305        }
306    }
307}
308
309impl KrpcResponse {
310    /// The responding node's ID.
311    #[must_use]
312    pub fn sender_id(&self) -> &Id20 {
313        match self {
314            Self::NodeId { id } | Self::FindNode { id, .. } | Self::GetItem { id, .. } => id,
315            Self::GetPeers(gp) => &gp.id,
316            Self::SampleInfohashes(si) => &si.id,
317        }
318    }
319}
320
321// ---- Encoding ----
322
323impl KrpcMessage {
324    /// Encode this message to bencode bytes.
325    ///
326    /// # Errors
327    ///
328    /// Returns an error if bencode serialization fails.
329    pub fn to_bytes(&self) -> Result<Vec<u8>> {
330        let mut dict = BTreeMap::<Vec<u8>, BencodeValue>::new();
331        dict.insert(
332            b"t".to_vec(),
333            BencodeValue::Bytes(self.transaction_id.0.to_vec()),
334        );
335
336        if let Some(addr) = &self.sender_ip {
337            let ip_bytes = encode_compact_addr(addr);
338            dict.insert(b"ip".to_vec(), BencodeValue::Bytes(ip_bytes));
339        }
340
341        // BEP 43: include `ro` flag when set
342        if self.read_only {
343            dict.insert(b"ro".to_vec(), BencodeValue::Integer(1));
344        }
345
346        match &self.body {
347            KrpcBody::Query(query) => {
348                dict.insert(b"y".to_vec(), BencodeValue::Bytes(b"q".to_vec()));
349                dict.insert(
350                    b"q".to_vec(),
351                    BencodeValue::Bytes(query.method_name().as_bytes().to_vec()),
352                );
353                dict.insert(b"a".to_vec(), encode_query_args(query));
354            }
355            KrpcBody::Response(resp) => {
356                dict.insert(b"y".to_vec(), BencodeValue::Bytes(b"r".to_vec()));
357                dict.insert(b"r".to_vec(), encode_response_values(resp));
358            }
359            KrpcBody::Error { code, message } => {
360                dict.insert(b"y".to_vec(), BencodeValue::Bytes(b"e".to_vec()));
361                dict.insert(
362                    b"e".to_vec(),
363                    BencodeValue::List(vec![
364                        BencodeValue::Integer(*code),
365                        BencodeValue::Bytes(message.as_bytes().to_vec()),
366                    ]),
367                );
368            }
369        }
370
371        bencode::to_bytes(&BencodeValue::Dict(dict)).map_err(Error::from)
372    }
373
374    /// Decode from bencode bytes.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error if the data is not valid bencode or the message
379    /// structure is malformed.
380    pub fn from_bytes(data: &[u8]) -> Result<Self> {
381        let value: BencodeValue = bencode::from_bytes(data)?;
382        let dict = value
383            .as_dict()
384            .ok_or_else(|| Error::InvalidMessage("top-level value is not a dict".into()))?;
385
386        let txn_bytes = dict_bytes(dict, b"t")?;
387        let transaction_id = TransactionId::from_bytes(txn_bytes)?;
388
389        let msg_type = dict_str(dict, b"y")?;
390        let body = match msg_type {
391            b"q" => {
392                let method = dict_str(dict, b"q")?;
393                let args = dict_dict(dict, b"a")?;
394                KrpcBody::Query(decode_query(method, args)?)
395            }
396            b"r" => {
397                let values = dict_dict(dict, b"r")?;
398                KrpcBody::Response(decode_response(values, None)?)
399            }
400            b"e" => {
401                let err_list = dict
402                    .get(&b"e"[..])
403                    .and_then(|v| v.as_list())
404                    .ok_or_else(|| Error::InvalidMessage("missing 'e' list".into()))?;
405                if err_list.len() < 2 {
406                    return Err(Error::InvalidMessage("error list too short".into()));
407                }
408                let code = err_list[0]
409                    .as_int()
410                    .ok_or_else(|| Error::InvalidMessage("error code not integer".into()))?;
411                let message = err_list[1]
412                    .as_bytes_raw()
413                    .map(|b| String::from_utf8_lossy(b).into_owned())
414                    .ok_or_else(|| Error::InvalidMessage("error message not string".into()))?;
415                KrpcBody::Error { code, message }
416            }
417            other => {
418                return Err(Error::InvalidMessage(format!(
419                    "unknown message type: {}",
420                    String::from_utf8_lossy(other)
421                )));
422            }
423        };
424
425        let sender_ip = dict
426            .get(&b"ip"[..])
427            .and_then(|v| v.as_bytes_raw())
428            .and_then(decode_compact_addr);
429
430        // BEP 43: extract read-only flag
431        let read_only = dict
432            .get(&b"ro"[..])
433            .and_then(irontide_bencode::BencodeValue::as_int)
434            .is_some_and(|i| i != 0);
435
436        Ok(Self {
437            transaction_id,
438            body,
439            sender_ip,
440            read_only,
441        })
442    }
443
444    /// Decode from bencode bytes with a query-method hint for response disambiguation.
445    ///
446    /// When you know which query method this response answers (e.g. `"get"` vs
447    /// `"get_peers"`), pass it here so the decoder picks the correct response
448    /// variant. This resolves ambiguity for BEP 44 "not found" responses that
449    /// share the same wire shape as `get_peers` responses (token + nodes only).
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if the data is not valid bencode or the message
454    /// structure is malformed.
455    pub fn from_bytes_with_query_hint(data: &[u8], query_method: &str) -> Result<Self> {
456        let value: BencodeValue = bencode::from_bytes(data)?;
457        let dict = value
458            .as_dict()
459            .ok_or_else(|| Error::InvalidMessage("top-level value is not a dict".into()))?;
460
461        let txn_bytes = dict_bytes(dict, b"t")?;
462        let transaction_id = TransactionId::from_bytes(txn_bytes)?;
463
464        let msg_type = dict_str(dict, b"y")?;
465        let body = match msg_type {
466            b"q" => {
467                let method = dict_str(dict, b"q")?;
468                let args = dict_dict(dict, b"a")?;
469                KrpcBody::Query(decode_query(method, args)?)
470            }
471            b"r" => {
472                let values = dict_dict(dict, b"r")?;
473                KrpcBody::Response(decode_response(values, Some(query_method))?)
474            }
475            b"e" => {
476                let err_list = dict
477                    .get(&b"e"[..])
478                    .and_then(|v| v.as_list())
479                    .ok_or_else(|| Error::InvalidMessage("missing 'e' list".into()))?;
480                if err_list.len() < 2 {
481                    return Err(Error::InvalidMessage("error list too short".into()));
482                }
483                let code = err_list[0]
484                    .as_int()
485                    .ok_or_else(|| Error::InvalidMessage("error code not integer".into()))?;
486                let message = err_list[1]
487                    .as_bytes_raw()
488                    .map(|b| String::from_utf8_lossy(b).into_owned())
489                    .ok_or_else(|| Error::InvalidMessage("error message not string".into()))?;
490                KrpcBody::Error { code, message }
491            }
492            other => {
493                return Err(Error::InvalidMessage(format!(
494                    "unknown message type: {}",
495                    String::from_utf8_lossy(other)
496                )));
497            }
498        };
499
500        let sender_ip = dict
501            .get(&b"ip"[..])
502            .and_then(|v| v.as_bytes_raw())
503            .and_then(decode_compact_addr);
504
505        // BEP 43: extract read-only flag
506        let read_only = dict
507            .get(&b"ro"[..])
508            .and_then(irontide_bencode::BencodeValue::as_int)
509            .is_some_and(|i| i != 0);
510
511        Ok(Self {
512            transaction_id,
513            body,
514            sender_ip,
515            read_only,
516        })
517    }
518}
519
520// ---- Internal encoding helpers ----
521
522fn encode_query_args(query: &KrpcQuery) -> BencodeValue {
523    let mut args = BTreeMap::<Vec<u8>, BencodeValue>::new();
524    match query {
525        KrpcQuery::Ping { id } => {
526            args.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
527        }
528        KrpcQuery::FindNode { id, target, want } => {
529            args.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
530            args.insert(b"target".to_vec(), BencodeValue::Bytes(target.0.to_vec()));
531            if let Some(w) = want {
532                encode_want(&mut args, w);
533            }
534        }
535        KrpcQuery::SampleInfohashes { id, target } => {
536            args.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
537            args.insert(b"target".to_vec(), BencodeValue::Bytes(target.0.to_vec()));
538        }
539        KrpcQuery::GetPeers {
540            id,
541            info_hash,
542            noseed,
543            scrape,
544            want,
545        } => {
546            args.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
547            args.insert(
548                b"info_hash".to_vec(),
549                BencodeValue::Bytes(info_hash.0.to_vec()),
550            );
551            if let Some(ns) = noseed {
552                args.insert(b"noseed".to_vec(), BencodeValue::Integer(*ns));
553            }
554            if let Some(sc) = scrape {
555                args.insert(b"scrape".to_vec(), BencodeValue::Integer(*sc));
556            }
557            if let Some(w) = want {
558                encode_want(&mut args, w);
559            }
560        }
561        KrpcQuery::AnnouncePeer {
562            id,
563            info_hash,
564            port,
565            implied_port,
566            token,
567        } => {
568            args.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
569            if *implied_port {
570                args.insert(b"implied_port".to_vec(), BencodeValue::Integer(1));
571            }
572            args.insert(
573                b"info_hash".to_vec(),
574                BencodeValue::Bytes(info_hash.0.to_vec()),
575            );
576            args.insert(b"port".to_vec(), BencodeValue::Integer(i64::from(*port)));
577            args.insert(b"token".to_vec(), BencodeValue::Bytes(token.clone()));
578        }
579        KrpcQuery::Get { id, target, seq } => {
580            args.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
581            if let Some(seq) = seq {
582                args.insert(b"seq".to_vec(), BencodeValue::Integer(*seq));
583            }
584            args.insert(b"target".to_vec(), BencodeValue::Bytes(target.0.to_vec()));
585        }
586        KrpcQuery::Put {
587            id,
588            token,
589            value,
590            key,
591            signature,
592            seq,
593            salt,
594            cas,
595        } => {
596            args.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
597            if let Some(cas) = cas {
598                args.insert(b"cas".to_vec(), BencodeValue::Integer(*cas));
599            }
600            if let Some(key) = key {
601                args.insert(b"k".to_vec(), BencodeValue::Bytes(key.to_vec()));
602            }
603            if let Some(salt) = salt
604                && !salt.is_empty()
605            {
606                args.insert(b"salt".to_vec(), BencodeValue::Bytes(salt.clone()));
607            }
608            if let Some(seq) = seq {
609                args.insert(b"seq".to_vec(), BencodeValue::Integer(*seq));
610            }
611            if let Some(sig) = signature {
612                args.insert(b"sig".to_vec(), BencodeValue::Bytes(sig.to_vec()));
613            }
614            args.insert(b"token".to_vec(), BencodeValue::Bytes(token.clone()));
615            args.insert(b"v".to_vec(), BencodeValue::Bytes(value.clone()));
616        }
617    }
618    BencodeValue::Dict(args)
619}
620
621fn encode_response_values(resp: &KrpcResponse) -> BencodeValue {
622    let mut values = BTreeMap::<Vec<u8>, BencodeValue>::new();
623    match resp {
624        KrpcResponse::NodeId { id } => {
625            values.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
626        }
627        KrpcResponse::FindNode { id, nodes, nodes6 } => {
628            values.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
629            values.insert(
630                b"nodes".to_vec(),
631                BencodeValue::Bytes(encode_compact_nodes(nodes)),
632            );
633            if !nodes6.is_empty() {
634                values.insert(
635                    b"nodes6".to_vec(),
636                    BencodeValue::Bytes(encode_compact_nodes6(nodes6)),
637                );
638            }
639        }
640        KrpcResponse::GetPeers(gp) => {
641            values.insert(b"id".to_vec(), BencodeValue::Bytes(gp.id.0.to_vec()));
642            if let Some(token) = &gp.token {
643                values.insert(b"token".to_vec(), BencodeValue::Bytes(token.clone()));
644            }
645            if !gp.peers.is_empty() {
646                let peer_list: Vec<BencodeValue> = gp
647                    .peers
648                    .iter()
649                    .map(|addr| match addr {
650                        SocketAddr::V4(v4) => {
651                            let mut buf = [0u8; 6];
652                            buf[..4].copy_from_slice(&v4.ip().octets());
653                            buf[4..6].copy_from_slice(&v4.port().to_be_bytes());
654                            BencodeValue::Bytes(buf.to_vec())
655                        }
656                        SocketAddr::V6(v6) => {
657                            let mut buf = [0u8; 18];
658                            buf[..16].copy_from_slice(&v6.ip().octets());
659                            buf[16..18].copy_from_slice(&v6.port().to_be_bytes());
660                            BencodeValue::Bytes(buf.to_vec())
661                        }
662                    })
663                    .collect();
664                values.insert(b"values".to_vec(), BencodeValue::List(peer_list));
665            }
666            if !gp.nodes.is_empty() {
667                values.insert(
668                    b"nodes".to_vec(),
669                    BencodeValue::Bytes(encode_compact_nodes(&gp.nodes)),
670                );
671            }
672            if !gp.nodes6.is_empty() {
673                values.insert(
674                    b"nodes6".to_vec(),
675                    BencodeValue::Bytes(encode_compact_nodes6(&gp.nodes6)),
676                );
677            }
678            if let Some(ref bfpe) = gp.bfpe {
679                values.insert(b"BFpe".to_vec(), BencodeValue::Bytes(bfpe.clone()));
680            }
681            if let Some(ref bfsd) = gp.bfsd {
682                values.insert(b"BFsd".to_vec(), BencodeValue::Bytes(bfsd.clone()));
683            }
684        }
685        KrpcResponse::GetItem {
686            id,
687            token,
688            nodes,
689            nodes6,
690            value,
691            key,
692            signature,
693            seq,
694        } => {
695            values.insert(b"id".to_vec(), BencodeValue::Bytes(id.0.to_vec()));
696            if let Some(key) = key {
697                values.insert(b"k".to_vec(), BencodeValue::Bytes(key.to_vec()));
698            }
699            if !nodes.is_empty() {
700                values.insert(
701                    b"nodes".to_vec(),
702                    BencodeValue::Bytes(encode_compact_nodes(nodes)),
703                );
704            }
705            if !nodes6.is_empty() {
706                values.insert(
707                    b"nodes6".to_vec(),
708                    BencodeValue::Bytes(encode_compact_nodes6(nodes6)),
709                );
710            }
711            if let Some(seq) = seq {
712                values.insert(b"seq".to_vec(), BencodeValue::Integer(*seq));
713            }
714            if let Some(sig) = signature {
715                values.insert(b"sig".to_vec(), BencodeValue::Bytes(sig.to_vec()));
716            }
717            if let Some(token) = token {
718                values.insert(b"token".to_vec(), BencodeValue::Bytes(token.clone()));
719            }
720            if let Some(v) = value {
721                values.insert(b"v".to_vec(), BencodeValue::Bytes(v.clone()));
722            }
723        }
724        KrpcResponse::SampleInfohashes(si) => {
725            values.insert(b"id".to_vec(), BencodeValue::Bytes(si.id.0.to_vec()));
726            values.insert(b"interval".to_vec(), BencodeValue::Integer(si.interval));
727            if !si.nodes.is_empty() {
728                values.insert(
729                    b"nodes".to_vec(),
730                    BencodeValue::Bytes(encode_compact_nodes(&si.nodes)),
731                );
732            }
733            values.insert(b"num".to_vec(), BencodeValue::Integer(si.num));
734            // BEP 51: "samples" is always present, even if empty
735            let mut samples_buf = Vec::with_capacity(si.samples.len() * 20);
736            for hash in &si.samples {
737                samples_buf.extend_from_slice(hash.as_bytes());
738            }
739            values.insert(b"samples".to_vec(), BencodeValue::Bytes(samples_buf));
740        }
741    }
742    BencodeValue::Dict(values)
743}
744
745// ---- BEP 45 want helpers ----
746
747fn encode_want(args: &mut BTreeMap<Vec<u8>, BencodeValue>, want: &[WantFamily]) {
748    let list: Vec<BencodeValue> = want
749        .iter()
750        .map(|w| BencodeValue::Bytes(w.as_bytes().to_vec()))
751        .collect();
752    args.insert(b"want".to_vec(), BencodeValue::List(list));
753}
754
755fn decode_want(args: &BTreeMap<Vec<u8>, BencodeValue>) -> Option<Vec<WantFamily>> {
756    let list = args.get(&b"want"[..])?.as_list()?;
757    let families: Vec<WantFamily> = list
758        .iter()
759        .filter_map(|v| v.as_bytes_raw().and_then(WantFamily::from_bytes))
760        .collect();
761    if families.is_empty() {
762        None
763    } else {
764        Some(families)
765    }
766}
767
768// ---- Compact address helpers (BEP 42 `ip` field) ----
769
770use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
771
772/// Encode a socket address to compact binary format (BEP 42 `ip` field).
773fn encode_compact_addr(addr: &SocketAddr) -> Vec<u8> {
774    match addr {
775        SocketAddr::V4(v4) => {
776            let mut buf = Vec::with_capacity(6);
777            buf.extend_from_slice(&v4.ip().octets());
778            buf.extend_from_slice(&v4.port().to_be_bytes());
779            buf
780        }
781        SocketAddr::V6(v6) => {
782            let mut buf = Vec::with_capacity(18);
783            buf.extend_from_slice(&v6.ip().octets());
784            buf.extend_from_slice(&v6.port().to_be_bytes());
785            buf
786        }
787    }
788}
789
790/// Decode a compact binary socket address (BEP 42 `ip` field).
791fn decode_compact_addr(data: &[u8]) -> Option<SocketAddr> {
792    match data.len() {
793        6 => {
794            let ip = Ipv4Addr::new(data[0], data[1], data[2], data[3]);
795            let port = u16::from_be_bytes([data[4], data[5]]);
796            Some(SocketAddr::V4(SocketAddrV4::new(ip, port)))
797        }
798        18 => {
799            let ip = Ipv6Addr::from(<[u8; 16]>::try_from(&data[..16]).unwrap());
800            let port = u16::from_be_bytes([data[16], data[17]]);
801            Some(SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)))
802        }
803        _ => None,
804    }
805}
806
807// ---- Internal decoding helpers ----
808
809fn decode_query(method: &[u8], args: &BTreeMap<Vec<u8>, BencodeValue>) -> Result<KrpcQuery> {
810    let id = args_id20(args, b"id")?;
811    match method {
812        b"ping" => Ok(KrpcQuery::Ping { id }),
813        b"find_node" => {
814            let target = args_id20(args, b"target")?;
815            let want = decode_want(args);
816            Ok(KrpcQuery::FindNode { id, target, want })
817        }
818        b"get_peers" => {
819            let info_hash = args_id20(args, b"info_hash")?;
820            let noseed = args
821                .get(&b"noseed"[..])
822                .and_then(irontide_bencode::BencodeValue::as_int);
823            let scrape = args
824                .get(&b"scrape"[..])
825                .and_then(irontide_bencode::BencodeValue::as_int);
826            let want = decode_want(args);
827            Ok(KrpcQuery::GetPeers {
828                id,
829                info_hash,
830                noseed,
831                scrape,
832                want,
833            })
834        }
835        b"announce_peer" => {
836            let info_hash = args_id20(args, b"info_hash")?;
837            let implied_port = args
838                .get(&b"implied_port"[..])
839                .and_then(irontide_bencode::BencodeValue::as_int)
840                .unwrap_or(0)
841                != 0;
842            // L5 (M241): `port` is attacker-controlled bencode. The previous
843            // `args_int(...)? as u16` silently truncated out-of-range values
844            // (70000 -> 4464, -1 -> 65535), producing a bogus non-connecting
845            // peer entry.
846            //
847            // Per BEP 5, when implied_port is set the requester's source UDP
848            // port is authoritative and `port` is ignored downstream, so we must
849            // not reject the query on an out-of-range / zero / absent port there
850            // (real clients send junk in that field) — normalize it to 0. When
851            // implied_port is unset, `port` is authoritative: range-check it and
852            // reject 0.
853            let port = if implied_port {
854                args.get(&b"port"[..])
855                    .and_then(irontide_bencode::BencodeValue::as_int)
856                    .and_then(|p| u16::try_from(p).ok())
857                    .unwrap_or(0)
858            } else {
859                let port_raw = args_int(args, b"port")?;
860                let port = u16::try_from(port_raw).map_err(|_| {
861                    Error::InvalidMessage(format!(
862                        "announce_peer 'port' {port_raw} is outside the valid range 0..=65535"
863                    ))
864                })?;
865                if port == 0 {
866                    return Err(Error::InvalidMessage(
867                        "announce_peer 'port' must not be 0 when implied_port is unset".into(),
868                    ));
869                }
870                port
871            };
872            let token = args
873                .get(&b"token"[..])
874                .and_then(|v| v.as_bytes_raw())
875                .map(<[u8]>::to_vec)
876                .ok_or_else(|| Error::InvalidMessage("missing 'token' in announce_peer".into()))?;
877            Ok(KrpcQuery::AnnouncePeer {
878                id,
879                info_hash,
880                port,
881                implied_port,
882                token,
883            })
884        }
885        b"get" => {
886            let target = args_id20(args, b"target")?;
887            let seq = args
888                .get(&b"seq"[..])
889                .and_then(irontide_bencode::BencodeValue::as_int);
890            Ok(KrpcQuery::Get { id, target, seq })
891        }
892        b"put" => {
893            let token = args
894                .get(&b"token"[..])
895                .and_then(|v| v.as_bytes_raw())
896                .map(<[u8]>::to_vec)
897                .ok_or_else(|| Error::InvalidMessage("missing 'token' in put".into()))?;
898            let value = args
899                .get(&b"v"[..])
900                .and_then(|v| v.as_bytes_raw())
901                .map(<[u8]>::to_vec)
902                .ok_or_else(|| Error::InvalidMessage("missing 'v' in put".into()))?;
903            let key = args
904                .get(&b"k"[..])
905                .and_then(|v| v.as_bytes_raw())
906                .and_then(|b| <[u8; 32]>::try_from(b).ok());
907            let signature = args
908                .get(&b"sig"[..])
909                .and_then(|v| v.as_bytes_raw())
910                .and_then(|b| <[u8; 64]>::try_from(b).ok());
911            let seq = args
912                .get(&b"seq"[..])
913                .and_then(irontide_bencode::BencodeValue::as_int);
914            let salt = args
915                .get(&b"salt"[..])
916                .and_then(|v| v.as_bytes_raw())
917                .map(<[u8]>::to_vec);
918            let cas = args
919                .get(&b"cas"[..])
920                .and_then(irontide_bencode::BencodeValue::as_int);
921            Ok(KrpcQuery::Put {
922                id,
923                token,
924                value,
925                key,
926                signature,
927                seq,
928                salt,
929                cas,
930            })
931        }
932        b"sample_infohashes" => {
933            let target = args_id20(args, b"target")?;
934            Ok(KrpcQuery::SampleInfohashes { id, target })
935        }
936        _ => Err(Error::InvalidMessage(format!(
937            "unknown query method: {}",
938            String::from_utf8_lossy(method)
939        ))),
940    }
941}
942
943/// Decode a KRPC response from bencoded values.
944///
945/// The optional `query_method` hint disambiguates responses that share the same
946/// wire shape (e.g. BEP 44 `get` vs BEP 5 `get_peers` — both may carry only
947/// `token` + `nodes`). When the caller knows the originating query method, pass
948/// `Some("get")` or `Some("get_peers")` to force the correct variant. Pass
949/// `None` to use heuristic detection (suitable for standalone decoding).
950fn decode_response(
951    values: &BTreeMap<Vec<u8>, BencodeValue>,
952    query_method: Option<&str>,
953) -> Result<KrpcResponse> {
954    let id = args_id20(values, b"id")?;
955
956    // If the caller tells us this is a BEP 44 get response, decode as GetItem
957    // regardless of which fields are present (handles "not found" case).
958    if query_method == Some("get") {
959        return decode_get_item_response(id, values);
960    }
961
962    // sample_infohashes response (BEP 51): has "samples" + "interval" + "num"
963    let has_samples = values.contains_key(&b"samples"[..]);
964    let has_interval = values.contains_key(&b"interval"[..]);
965
966    if has_samples && has_interval {
967        let interval = values
968            .get(&b"interval"[..])
969            .and_then(irontide_bencode::BencodeValue::as_int)
970            .unwrap_or(0);
971        let num = values
972            .get(&b"num"[..])
973            .and_then(irontide_bencode::BencodeValue::as_int)
974            .unwrap_or(0);
975
976        let samples_bytes = values
977            .get(&b"samples"[..])
978            .and_then(|v| v.as_bytes_raw())
979            .unwrap_or(&[]);
980        let mut samples = Vec::new();
981        if samples_bytes.len().is_multiple_of(20) {
982            for chunk in samples_bytes.chunks_exact(20) {
983                if let Ok(hash) = Id20::from_bytes(chunk) {
984                    samples.push(hash);
985                }
986            }
987        }
988
989        let nodes =
990            if let Some(nodes_bytes) = values.get(&b"nodes"[..]).and_then(|v| v.as_bytes_raw()) {
991                parse_compact_nodes(nodes_bytes)?
992            } else {
993                Vec::new()
994            };
995
996        return Ok(KrpcResponse::SampleInfohashes(SampleInfohashesResponse {
997            id,
998            interval,
999            num,
1000            samples,
1001            nodes,
1002        }));
1003    }
1004
1005    // BEP 44 get response heuristic: has "k" (mutable key), "sig" (signature),
1006    // or "v" without "values" (immutable value — not a get_peers peer list).
1007    let has_values = values.contains_key(&b"values"[..]);
1008    let has_v = values.contains_key(&b"v"[..]);
1009    let has_k = values.contains_key(&b"k"[..]);
1010    let has_sig = values.contains_key(&b"sig"[..]);
1011    let has_seq = values.contains_key(&b"seq"[..]);
1012
1013    if has_k || has_sig || (has_v && !has_values) || (has_seq && !has_values) {
1014        return decode_get_item_response(id, values);
1015    }
1016
1017    // get_peers response: has "values" (peers) or "nodes" (closer nodes) + optional "token"
1018    let has_token = values.contains_key(&b"token"[..]);
1019
1020    if has_values || has_token {
1021        let token = values
1022            .get(&b"token"[..])
1023            .and_then(|v| v.as_bytes_raw())
1024            .map(<[u8]>::to_vec);
1025
1026        let mut peers = Vec::new();
1027        if let Some(BencodeValue::List(peer_list)) = values.get(&b"values"[..]) {
1028            for item in peer_list {
1029                if let Some(data) = item.as_bytes_raw() {
1030                    match data.len() {
1031                        6 => {
1032                            let ip = Ipv4Addr::new(data[0], data[1], data[2], data[3]);
1033                            let port = u16::from_be_bytes([data[4], data[5]]);
1034                            peers.push(SocketAddr::V4(SocketAddrV4::new(ip, port)));
1035                        }
1036                        18 => {
1037                            let ip = Ipv6Addr::from(<[u8; 16]>::try_from(&data[..16]).unwrap());
1038                            let port = u16::from_be_bytes([data[16], data[17]]);
1039                            peers.push(SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)));
1040                        }
1041                        _ => {} // skip unknown sizes
1042                    }
1043                }
1044            }
1045        }
1046
1047        let nodes =
1048            if let Some(nodes_bytes) = values.get(&b"nodes"[..]).and_then(|v| v.as_bytes_raw()) {
1049                parse_compact_nodes(nodes_bytes)?
1050            } else {
1051                Vec::new()
1052            };
1053
1054        let nodes6 =
1055            if let Some(nodes6_bytes) = values.get(&b"nodes6"[..]).and_then(|v| v.as_bytes_raw()) {
1056                parse_compact_nodes6(nodes6_bytes)?
1057            } else {
1058                Vec::new()
1059            };
1060
1061        let bfpe = values
1062            .get(&b"BFpe"[..])
1063            .and_then(|v| v.as_bytes_raw())
1064            .map(<[u8]>::to_vec);
1065        let bfsd = values
1066            .get(&b"BFsd"[..])
1067            .and_then(|v| v.as_bytes_raw())
1068            .map(<[u8]>::to_vec);
1069
1070        return Ok(KrpcResponse::GetPeers(GetPeersResponse {
1071            id,
1072            token,
1073            peers,
1074            nodes,
1075            nodes6,
1076            bfpe,
1077            bfsd,
1078        }));
1079    }
1080
1081    // find_node response: has "nodes" or "nodes6"
1082    let has_nodes = values.contains_key(&b"nodes"[..]);
1083    let has_nodes6 = values.contains_key(&b"nodes6"[..]);
1084
1085    if has_nodes || has_nodes6 {
1086        let nodes =
1087            if let Some(nodes_bytes) = values.get(&b"nodes"[..]).and_then(|v| v.as_bytes_raw()) {
1088                parse_compact_nodes(nodes_bytes)?
1089            } else {
1090                Vec::new()
1091            };
1092
1093        let nodes6 =
1094            if let Some(nodes6_bytes) = values.get(&b"nodes6"[..]).and_then(|v| v.as_bytes_raw()) {
1095                parse_compact_nodes6(nodes6_bytes)?
1096            } else {
1097                Vec::new()
1098            };
1099
1100        return Ok(KrpcResponse::FindNode { id, nodes, nodes6 });
1101    }
1102
1103    // Plain ID response (ping, announce_peer)
1104    Ok(KrpcResponse::NodeId { id })
1105}
1106
1107/// Decode a BEP 44 `GetItem` response from its value dict.
1108fn decode_get_item_response(
1109    id: Id20,
1110    values: &BTreeMap<Vec<u8>, BencodeValue>,
1111) -> Result<KrpcResponse> {
1112    let token = values
1113        .get(&b"token"[..])
1114        .and_then(|v| v.as_bytes_raw())
1115        .map(<[u8]>::to_vec);
1116
1117    let nodes = if let Some(nodes_bytes) = values.get(&b"nodes"[..]).and_then(|v| v.as_bytes_raw())
1118    {
1119        parse_compact_nodes(nodes_bytes)?
1120    } else {
1121        Vec::new()
1122    };
1123
1124    let nodes6 =
1125        if let Some(nodes6_bytes) = values.get(&b"nodes6"[..]).and_then(|v| v.as_bytes_raw()) {
1126            parse_compact_nodes6(nodes6_bytes)?
1127        } else {
1128            Vec::new()
1129        };
1130
1131    let value = values
1132        .get(&b"v"[..])
1133        .and_then(|v| v.as_bytes_raw())
1134        .map(<[u8]>::to_vec);
1135
1136    let key = values
1137        .get(&b"k"[..])
1138        .and_then(|v| v.as_bytes_raw())
1139        .and_then(|b| <[u8; 32]>::try_from(b).ok());
1140
1141    let signature = values
1142        .get(&b"sig"[..])
1143        .and_then(|v| v.as_bytes_raw())
1144        .and_then(|b| <[u8; 64]>::try_from(b).ok());
1145
1146    let seq = values
1147        .get(&b"seq"[..])
1148        .and_then(irontide_bencode::BencodeValue::as_int);
1149
1150    Ok(KrpcResponse::GetItem {
1151        id,
1152        token,
1153        nodes,
1154        nodes6,
1155        value,
1156        key,
1157        signature,
1158        seq,
1159    })
1160}
1161
1162// ---- Dict access helpers ----
1163
1164fn dict_bytes<'a>(dict: &'a BTreeMap<Vec<u8>, BencodeValue>, key: &[u8]) -> Result<&'a [u8]> {
1165    dict.get(key).and_then(|v| v.as_bytes_raw()).ok_or_else(|| {
1166        Error::InvalidMessage(format!(
1167            "missing or invalid key '{}'",
1168            String::from_utf8_lossy(key)
1169        ))
1170    })
1171}
1172
1173fn dict_str<'a>(dict: &'a BTreeMap<Vec<u8>, BencodeValue>, key: &[u8]) -> Result<&'a [u8]> {
1174    dict_bytes(dict, key)
1175}
1176
1177fn dict_dict<'a>(
1178    dict: &'a BTreeMap<Vec<u8>, BencodeValue>,
1179    key: &[u8],
1180) -> Result<&'a BTreeMap<Vec<u8>, BencodeValue>> {
1181    dict.get(key).and_then(|v| v.as_dict()).ok_or_else(|| {
1182        Error::InvalidMessage(format!(
1183            "missing or invalid dict key '{}'",
1184            String::from_utf8_lossy(key)
1185        ))
1186    })
1187}
1188
1189fn args_id20(args: &BTreeMap<Vec<u8>, BencodeValue>, key: &[u8]) -> Result<Id20> {
1190    let bytes = args
1191        .get(key)
1192        .and_then(|v| v.as_bytes_raw())
1193        .ok_or_else(|| {
1194            Error::InvalidMessage(format!(
1195                "missing '{}' in args",
1196                String::from_utf8_lossy(key)
1197            ))
1198        })?;
1199    Id20::from_bytes(bytes).map_err(|e| Error::InvalidMessage(e.to_string()))
1200}
1201
1202fn args_int(args: &BTreeMap<Vec<u8>, BencodeValue>, key: &[u8]) -> Result<i64> {
1203    args.get(key)
1204        .and_then(irontide_bencode::BencodeValue::as_int)
1205        .ok_or_else(|| {
1206            Error::InvalidMessage(format!(
1207                "missing '{}' integer in args",
1208                String::from_utf8_lossy(key)
1209            ))
1210        })
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215    use super::*;
1216    use pretty_assertions::assert_eq;
1217
1218    fn test_id() -> Id20 {
1219        Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap()
1220    }
1221
1222    fn target_id() -> Id20 {
1223        Id20::from_hex("0000000000000000000000000000000000000001").unwrap()
1224    }
1225
1226    #[test]
1227    fn ping_query_round_trip() {
1228        let msg = KrpcMessage {
1229            transaction_id: TransactionId::from_u16(42),
1230            body: KrpcBody::Query(KrpcQuery::Ping { id: test_id() }),
1231            sender_ip: None,
1232            read_only: false,
1233        };
1234        let bytes = msg.to_bytes().unwrap();
1235        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1236        assert_eq!(msg, decoded);
1237    }
1238
1239    #[test]
1240    fn ping_response_round_trip() {
1241        let msg = KrpcMessage {
1242            transaction_id: TransactionId::from_u16(42),
1243            body: KrpcBody::Response(KrpcResponse::NodeId { id: test_id() }),
1244            sender_ip: None,
1245            read_only: false,
1246        };
1247        let bytes = msg.to_bytes().unwrap();
1248        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1249        assert_eq!(msg, decoded);
1250    }
1251
1252    #[test]
1253    fn find_node_query_round_trip() {
1254        let msg = KrpcMessage {
1255            transaction_id: TransactionId::from_u16(100),
1256            body: KrpcBody::Query(KrpcQuery::FindNode {
1257                id: test_id(),
1258                target: target_id(),
1259                want: None,
1260            }),
1261            sender_ip: None,
1262            read_only: false,
1263        };
1264        let bytes = msg.to_bytes().unwrap();
1265        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1266        assert_eq!(msg, decoded);
1267    }
1268
1269    #[test]
1270    fn find_node_response_round_trip() {
1271        let nodes = vec![CompactNodeInfo {
1272            id: target_id(),
1273            addr: "10.0.0.1:6881".parse().unwrap(),
1274        }];
1275        let msg = KrpcMessage {
1276            transaction_id: TransactionId::from_u16(100),
1277            body: KrpcBody::Response(KrpcResponse::FindNode {
1278                id: test_id(),
1279                nodes,
1280                nodes6: Vec::new(),
1281            }),
1282            sender_ip: None,
1283            read_only: false,
1284        };
1285        let bytes = msg.to_bytes().unwrap();
1286        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1287        assert_eq!(msg, decoded);
1288    }
1289
1290    #[test]
1291    fn get_peers_query_round_trip() {
1292        let msg = KrpcMessage {
1293            transaction_id: TransactionId::from_u16(200),
1294            body: KrpcBody::Query(KrpcQuery::GetPeers {
1295                id: test_id(),
1296                info_hash: target_id(),
1297                noseed: None,
1298                scrape: None,
1299                want: None,
1300            }),
1301            sender_ip: None,
1302            read_only: false,
1303        };
1304        let bytes = msg.to_bytes().unwrap();
1305        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1306        assert_eq!(msg, decoded);
1307    }
1308
1309    #[test]
1310    fn get_peers_response_with_peers_round_trip() {
1311        let msg = KrpcMessage {
1312            transaction_id: TransactionId::from_u16(200),
1313            body: KrpcBody::Response(KrpcResponse::GetPeers(GetPeersResponse {
1314                id: test_id(),
1315                token: Some(b"aoeusnth".to_vec()),
1316                peers: vec!["192.168.1.1:6881".parse().unwrap()],
1317                nodes: Vec::new(),
1318                nodes6: Vec::new(),
1319                bfpe: None,
1320                bfsd: None,
1321            })),
1322            sender_ip: None,
1323            read_only: false,
1324        };
1325        let bytes = msg.to_bytes().unwrap();
1326        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1327        assert_eq!(msg, decoded);
1328    }
1329
1330    #[test]
1331    fn get_peers_response_with_nodes_round_trip() {
1332        let msg = KrpcMessage {
1333            transaction_id: TransactionId::from_u16(200),
1334            body: KrpcBody::Response(KrpcResponse::GetPeers(GetPeersResponse {
1335                id: test_id(),
1336                token: Some(b"token123".to_vec()),
1337                peers: Vec::new(),
1338                nodes: vec![CompactNodeInfo {
1339                    id: target_id(),
1340                    addr: "10.0.0.1:6881".parse().unwrap(),
1341                }],
1342                nodes6: Vec::new(),
1343                bfpe: None,
1344                bfsd: None,
1345            })),
1346            sender_ip: None,
1347            read_only: false,
1348        };
1349        let bytes = msg.to_bytes().unwrap();
1350        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1351        assert_eq!(msg, decoded);
1352    }
1353
1354    #[test]
1355    fn announce_peer_query_round_trip() {
1356        let msg = KrpcMessage {
1357            transaction_id: TransactionId::from_u16(300),
1358            body: KrpcBody::Query(KrpcQuery::AnnouncePeer {
1359                id: test_id(),
1360                info_hash: target_id(),
1361                port: 6881,
1362                implied_port: true,
1363                token: b"aoeusnth".to_vec(),
1364            }),
1365            sender_ip: None,
1366            read_only: false,
1367        };
1368        let bytes = msg.to_bytes().unwrap();
1369        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1370        assert_eq!(msg, decoded);
1371    }
1372
1373    #[test]
1374    fn error_message_round_trip() {
1375        let msg = KrpcMessage {
1376            transaction_id: TransactionId::from_u16(500),
1377            body: KrpcBody::Error {
1378                code: 201,
1379                message: "A Generic Error Occurred".into(),
1380            },
1381            sender_ip: None,
1382            read_only: false,
1383        };
1384        let bytes = msg.to_bytes().unwrap();
1385        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1386        assert_eq!(msg, decoded);
1387    }
1388
1389    #[test]
1390    fn decode_bep5_ping_example() {
1391        // BEP 5 example: ping query
1392        // d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe
1393        let data = b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe";
1394        let msg = KrpcMessage::from_bytes(data).unwrap();
1395        assert_eq!(msg.transaction_id.0, *b"aa");
1396        match &msg.body {
1397            KrpcBody::Query(KrpcQuery::Ping { id }) => {
1398                assert_eq!(id.as_bytes(), b"abcdefghij0123456789");
1399            }
1400            other => panic!("expected Ping query, got {other:?}"),
1401        }
1402    }
1403
1404    #[test]
1405    fn decode_bep5_error_example() {
1406        // BEP 5 example: generic error (corrected length: 24 chars)
1407        let data = b"d1:eli201e24:A Generic Error Occurrede1:t2:aa1:y1:ee";
1408        let msg = KrpcMessage::from_bytes(data).unwrap();
1409        assert_eq!(msg.transaction_id.0, *b"aa");
1410        match &msg.body {
1411            KrpcBody::Error { code, message } => {
1412                assert_eq!(*code, 201);
1413                assert_eq!(message, "A Generic Error Occurred");
1414            }
1415            other => panic!("expected Error, got {other:?}"),
1416        }
1417    }
1418
1419    #[test]
1420    fn transaction_id_from_single_byte() {
1421        let tid = TransactionId::from_bytes(&[0x42]).unwrap();
1422        assert_eq!(tid.0, [0x42, 0x00]);
1423    }
1424
1425    #[test]
1426    fn query_method_names() {
1427        assert_eq!(KrpcQuery::Ping { id: Id20::ZERO }.method_name(), "ping");
1428        assert_eq!(
1429            KrpcQuery::FindNode {
1430                id: Id20::ZERO,
1431                target: Id20::ZERO,
1432                want: None,
1433            }
1434            .method_name(),
1435            "find_node"
1436        );
1437    }
1438
1439    // --- IPv6 KRPC tests ---
1440
1441    #[test]
1442    fn find_node_response_with_nodes6_round_trip() {
1443        let nodes = vec![CompactNodeInfo {
1444            id: target_id(),
1445            addr: "10.0.0.1:6881".parse().unwrap(),
1446        }];
1447        let nodes6 = vec![CompactNodeInfo6 {
1448            id: target_id(),
1449            addr: "[2001:db8::1]:6881".parse().unwrap(),
1450        }];
1451        let msg = KrpcMessage {
1452            transaction_id: TransactionId::from_u16(100),
1453            body: KrpcBody::Response(KrpcResponse::FindNode {
1454                id: test_id(),
1455                nodes,
1456                nodes6,
1457            }),
1458            sender_ip: None,
1459            read_only: false,
1460        };
1461        let bytes = msg.to_bytes().unwrap();
1462        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1463        assert_eq!(msg, decoded);
1464    }
1465
1466    #[test]
1467    fn get_peers_response_with_nodes6_round_trip() {
1468        let nodes6 = vec![CompactNodeInfo6 {
1469            id: target_id(),
1470            addr: "[::1]:8080".parse().unwrap(),
1471        }];
1472        let msg = KrpcMessage {
1473            transaction_id: TransactionId::from_u16(200),
1474            body: KrpcBody::Response(KrpcResponse::GetPeers(GetPeersResponse {
1475                id: test_id(),
1476                token: Some(b"tok".to_vec()),
1477                peers: Vec::new(),
1478                nodes: Vec::new(),
1479                nodes6,
1480                bfpe: None,
1481                bfsd: None,
1482            })),
1483            sender_ip: None,
1484            read_only: false,
1485        };
1486        let bytes = msg.to_bytes().unwrap();
1487        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1488        assert_eq!(msg, decoded);
1489    }
1490
1491    #[test]
1492    fn get_peers_response_with_ipv6_peer_values() {
1493        let msg = KrpcMessage {
1494            transaction_id: TransactionId::from_u16(200),
1495            body: KrpcBody::Response(KrpcResponse::GetPeers(GetPeersResponse {
1496                id: test_id(),
1497                token: Some(b"tok".to_vec()),
1498                peers: vec![
1499                    "192.168.1.1:6881".parse().unwrap(),
1500                    "[2001:db8::1]:8080".parse().unwrap(),
1501                ],
1502                nodes: Vec::new(),
1503                nodes6: Vec::new(),
1504                bfpe: None,
1505                bfsd: None,
1506            })),
1507            sender_ip: None,
1508            read_only: false,
1509        };
1510        let bytes = msg.to_bytes().unwrap();
1511        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1512        assert_eq!(msg, decoded);
1513    }
1514
1515    // --- BEP 42 ip field tests ---
1516
1517    #[test]
1518    fn response_with_ip_field_round_trip() {
1519        let msg = KrpcMessage {
1520            transaction_id: TransactionId::from_u16(42),
1521            body: KrpcBody::Response(KrpcResponse::NodeId { id: test_id() }),
1522            sender_ip: Some("203.0.113.5:6881".parse().unwrap()),
1523            read_only: false,
1524        };
1525        let bytes = msg.to_bytes().unwrap();
1526        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1527        assert_eq!(decoded.sender_ip, Some("203.0.113.5:6881".parse().unwrap()));
1528    }
1529
1530    #[test]
1531    fn response_with_ipv6_ip_field_round_trip() {
1532        let msg = KrpcMessage {
1533            transaction_id: TransactionId::from_u16(42),
1534            body: KrpcBody::Response(KrpcResponse::NodeId { id: test_id() }),
1535            sender_ip: Some("[2001:db8::1]:6881".parse().unwrap()),
1536            read_only: false,
1537        };
1538        let bytes = msg.to_bytes().unwrap();
1539        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1540        assert_eq!(
1541            decoded.sender_ip,
1542            Some("[2001:db8::1]:6881".parse().unwrap())
1543        );
1544    }
1545
1546    #[test]
1547    fn message_without_ip_field_parses_as_none() {
1548        let msg = KrpcMessage {
1549            transaction_id: TransactionId::from_u16(42),
1550            body: KrpcBody::Response(KrpcResponse::NodeId { id: test_id() }),
1551            sender_ip: None,
1552            read_only: false,
1553        };
1554        let bytes = msg.to_bytes().unwrap();
1555        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1556        assert!(decoded.sender_ip.is_none());
1557    }
1558
1559    // --- BEP 44 KRPC tests ---
1560
1561    #[test]
1562    fn get_immutable_query_round_trip() {
1563        let msg = KrpcMessage {
1564            transaction_id: TransactionId::from_u16(400),
1565            body: KrpcBody::Query(KrpcQuery::Get {
1566                id: test_id(),
1567                target: target_id(),
1568                seq: None,
1569            }),
1570            sender_ip: None,
1571            read_only: false,
1572        };
1573        let bytes = msg.to_bytes().unwrap();
1574        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1575        assert_eq!(msg, decoded);
1576    }
1577
1578    #[test]
1579    fn get_mutable_query_with_seq_round_trip() {
1580        let msg = KrpcMessage {
1581            transaction_id: TransactionId::from_u16(401),
1582            body: KrpcBody::Query(KrpcQuery::Get {
1583                id: test_id(),
1584                target: target_id(),
1585                seq: Some(42),
1586            }),
1587            sender_ip: None,
1588            read_only: false,
1589        };
1590        let bytes = msg.to_bytes().unwrap();
1591        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1592        assert_eq!(msg, decoded);
1593    }
1594
1595    #[test]
1596    fn put_immutable_query_round_trip() {
1597        let msg = KrpcMessage {
1598            transaction_id: TransactionId::from_u16(402),
1599            body: KrpcBody::Query(KrpcQuery::Put {
1600                id: test_id(),
1601                token: b"tok12345".to_vec(),
1602                value: b"12:Hello World!".to_vec(),
1603                key: None,
1604                signature: None,
1605                seq: None,
1606                salt: None,
1607                cas: None,
1608            }),
1609            sender_ip: None,
1610            read_only: false,
1611        };
1612        let bytes = msg.to_bytes().unwrap();
1613        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1614        assert_eq!(msg, decoded);
1615    }
1616
1617    #[test]
1618    fn put_mutable_query_round_trip() {
1619        let key = [0xABu8; 32];
1620        let sig = [0xCDu8; 64];
1621        let msg = KrpcMessage {
1622            transaction_id: TransactionId::from_u16(403),
1623            body: KrpcBody::Query(KrpcQuery::Put {
1624                id: test_id(),
1625                token: b"tok12345".to_vec(),
1626                value: b"12:Hello World!".to_vec(),
1627                key: Some(key),
1628                signature: Some(sig),
1629                seq: Some(4),
1630                salt: Some(b"foobar".to_vec()),
1631                cas: Some(3),
1632            }),
1633            sender_ip: None,
1634            read_only: false,
1635        };
1636        let bytes = msg.to_bytes().unwrap();
1637        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1638        assert_eq!(msg, decoded);
1639    }
1640
1641    #[test]
1642    fn get_response_immutable_round_trip() {
1643        let msg = KrpcMessage {
1644            transaction_id: TransactionId::from_u16(404),
1645            body: KrpcBody::Response(KrpcResponse::GetItem {
1646                id: test_id(),
1647                token: Some(b"tok".to_vec()),
1648                nodes: Vec::new(),
1649                nodes6: Vec::new(),
1650                value: Some(b"12:Hello World!".to_vec()),
1651                key: None,
1652                signature: None,
1653                seq: None,
1654            }),
1655            sender_ip: None,
1656            read_only: false,
1657        };
1658        let bytes = msg.to_bytes().unwrap();
1659        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1660        assert_eq!(msg, decoded);
1661    }
1662
1663    #[test]
1664    fn get_response_mutable_round_trip() {
1665        let key = [0xABu8; 32];
1666        let sig = [0xCDu8; 64];
1667        let msg = KrpcMessage {
1668            transaction_id: TransactionId::from_u16(405),
1669            body: KrpcBody::Response(KrpcResponse::GetItem {
1670                id: test_id(),
1671                token: Some(b"tok".to_vec()),
1672                nodes: vec![CompactNodeInfo {
1673                    id: target_id(),
1674                    addr: "10.0.0.1:6881".parse().unwrap(),
1675                }],
1676                nodes6: Vec::new(),
1677                value: Some(b"4:test".to_vec()),
1678                key: Some(key),
1679                signature: Some(sig),
1680                seq: Some(7),
1681            }),
1682            sender_ip: None,
1683            read_only: false,
1684        };
1685        let bytes = msg.to_bytes().unwrap();
1686        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1687        assert_eq!(msg, decoded);
1688    }
1689
1690    #[test]
1691    fn get_response_not_found_with_hint_round_trip() {
1692        // "Not found" BEP 44 response: only token + nodes, no v/k/sig/seq.
1693        // Without the query_method hint this would be decoded as GetPeers.
1694        let msg = KrpcMessage {
1695            transaction_id: TransactionId::from_u16(406),
1696            body: KrpcBody::Response(KrpcResponse::GetItem {
1697                id: test_id(),
1698                token: Some(b"tok".to_vec()),
1699                nodes: vec![CompactNodeInfo {
1700                    id: target_id(),
1701                    addr: "10.0.0.1:6881".parse().unwrap(),
1702                }],
1703                nodes6: Vec::new(),
1704                value: None,
1705                key: None,
1706                signature: None,
1707                seq: None,
1708            }),
1709            sender_ip: None,
1710            read_only: false,
1711        };
1712        let bytes = msg.to_bytes().unwrap();
1713        // Use from_bytes_with_query_hint to force GetItem decoding
1714        let decoded = KrpcMessage::from_bytes_with_query_hint(&bytes, "get").unwrap();
1715        assert_eq!(msg, decoded);
1716    }
1717
1718    #[test]
1719    fn bep44_query_method_names() {
1720        assert_eq!(
1721            KrpcQuery::Get {
1722                id: Id20::ZERO,
1723                target: Id20::ZERO,
1724                seq: None,
1725            }
1726            .method_name(),
1727            "get"
1728        );
1729        assert_eq!(
1730            KrpcQuery::Put {
1731                id: Id20::ZERO,
1732                token: Vec::new(),
1733                value: Vec::new(),
1734                key: None,
1735                signature: None,
1736                seq: None,
1737                salt: None,
1738                cas: None,
1739            }
1740            .method_name(),
1741            "put"
1742        );
1743    }
1744
1745    // --- BEP 51 KRPC tests ---
1746
1747    #[test]
1748    fn sample_infohashes_query_round_trip() {
1749        let msg = KrpcMessage {
1750            transaction_id: TransactionId::from_u16(400),
1751            body: KrpcBody::Query(KrpcQuery::SampleInfohashes {
1752                id: test_id(),
1753                target: target_id(),
1754            }),
1755            sender_ip: None,
1756            read_only: false,
1757        };
1758        let bytes = msg.to_bytes().unwrap();
1759        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1760        assert_eq!(msg, decoded);
1761    }
1762
1763    #[test]
1764    fn sample_infohashes_response_round_trip() {
1765        let sample1 = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
1766        let sample2 = Id20::from_hex("0000000000000000000000000000000000000001").unwrap();
1767        let nodes = vec![CompactNodeInfo {
1768            id: target_id(),
1769            addr: "10.0.0.1:6881".parse().unwrap(),
1770        }];
1771        let msg = KrpcMessage {
1772            transaction_id: TransactionId::from_u16(400),
1773            body: KrpcBody::Response(KrpcResponse::SampleInfohashes(SampleInfohashesResponse {
1774                id: test_id(),
1775                interval: 300,
1776                num: 42,
1777                samples: vec![sample1, sample2],
1778                nodes,
1779            })),
1780            sender_ip: None,
1781            read_only: false,
1782        };
1783        let bytes = msg.to_bytes().unwrap();
1784        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1785        assert_eq!(msg, decoded);
1786    }
1787
1788    #[test]
1789    fn sample_infohashes_response_empty_samples() {
1790        let msg = KrpcMessage {
1791            transaction_id: TransactionId::from_u16(401),
1792            body: KrpcBody::Response(KrpcResponse::SampleInfohashes(SampleInfohashesResponse {
1793                id: test_id(),
1794                interval: 60,
1795                num: 0,
1796                samples: Vec::new(),
1797                nodes: Vec::new(),
1798            })),
1799            sender_ip: None,
1800            read_only: false,
1801        };
1802        let bytes = msg.to_bytes().unwrap();
1803        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1804        assert_eq!(msg, decoded);
1805    }
1806
1807    #[test]
1808    fn sample_infohashes_query_method_name() {
1809        assert_eq!(
1810            KrpcQuery::SampleInfohashes {
1811                id: Id20::ZERO,
1812                target: Id20::ZERO,
1813            }
1814            .method_name(),
1815            "sample_infohashes"
1816        );
1817    }
1818
1819    // --- BEP 43 read-only node tests ---
1820
1821    #[test]
1822    fn krpc_ro_flag_roundtrip() {
1823        // Encode a query with read_only: true, decode, verify the flag survives.
1824        let msg = KrpcMessage {
1825            transaction_id: TransactionId::from_u16(43),
1826            body: KrpcBody::Query(KrpcQuery::Ping { id: test_id() }),
1827            sender_ip: None,
1828            read_only: true,
1829        };
1830        let bytes = msg.to_bytes().unwrap();
1831        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1832        assert!(decoded.read_only, "ro flag should survive round-trip");
1833        assert_eq!(decoded.body, msg.body);
1834    }
1835
1836    #[test]
1837    fn krpc_ro_absent_defaults_false() {
1838        // A message without an `ro` field should decode with read_only == false.
1839        let msg = KrpcMessage {
1840            transaction_id: TransactionId::from_u16(43),
1841            body: KrpcBody::Query(KrpcQuery::Ping { id: test_id() }),
1842            sender_ip: None,
1843            read_only: false,
1844        };
1845        let bytes = msg.to_bytes().unwrap();
1846        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1847        assert!(!decoded.read_only, "absent ro should default to false");
1848    }
1849
1850    // --- BEP 33 DHT scrape tests ---
1851
1852    #[test]
1853    fn krpc_get_peers_with_scrape_flag() {
1854        let msg = KrpcMessage {
1855            transaction_id: TransactionId::from_u16(330),
1856            body: KrpcBody::Query(KrpcQuery::GetPeers {
1857                id: test_id(),
1858                info_hash: target_id(),
1859                noseed: None,
1860                scrape: Some(1),
1861                want: None,
1862            }),
1863            sender_ip: None,
1864            read_only: false,
1865        };
1866        let bytes = msg.to_bytes().unwrap();
1867        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1868        assert_eq!(msg, decoded);
1869        match &decoded.body {
1870            KrpcBody::Query(KrpcQuery::GetPeers { scrape, noseed, .. }) => {
1871                assert_eq!(*scrape, Some(1));
1872                assert_eq!(*noseed, None);
1873            }
1874            other => panic!("expected GetPeers query, got {other:?}"),
1875        }
1876    }
1877
1878    #[test]
1879    fn krpc_get_peers_response_with_bloom() {
1880        let bfpe_data = vec![0xAA; 256];
1881        let bfsd_data = vec![0x55; 256];
1882        let msg = KrpcMessage {
1883            transaction_id: TransactionId::from_u16(331),
1884            body: KrpcBody::Response(KrpcResponse::GetPeers(GetPeersResponse {
1885                id: test_id(),
1886                token: Some(b"tok".to_vec()),
1887                peers: vec!["192.168.1.1:6881".parse().unwrap()],
1888                nodes: Vec::new(),
1889                nodes6: Vec::new(),
1890                bfpe: Some(bfpe_data.clone()),
1891                bfsd: Some(bfsd_data.clone()),
1892            })),
1893            sender_ip: None,
1894            read_only: false,
1895        };
1896        let bytes = msg.to_bytes().unwrap();
1897        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
1898        assert_eq!(msg, decoded);
1899        match &decoded.body {
1900            KrpcBody::Response(KrpcResponse::GetPeers(gp)) => {
1901                assert_eq!(gp.bfpe.as_deref(), Some(bfpe_data.as_slice()));
1902                assert_eq!(gp.bfsd.as_deref(), Some(bfsd_data.as_slice()));
1903            }
1904            other => panic!("expected GetPeers response, got {other:?}"),
1905        }
1906    }
1907
1908    // --- BEP 5 spec vector conformance tests ---
1909
1910    /// Helper: build an Id20 from a 20-byte ASCII string (as used in BEP 5 examples).
1911    fn bep5_id(ascii: &[u8; 20]) -> Id20 {
1912        Id20(*ascii)
1913    }
1914
1915    #[test]
1916    fn krpc_decode_find_node_query_spec() {
1917        // BEP 5 example: find_node query
1918        let data = b"d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe";
1919        let msg = KrpcMessage::from_bytes(data).unwrap();
1920        assert_eq!(msg.transaction_id.0, *b"aa");
1921        assert!(!msg.read_only);
1922        match &msg.body {
1923            KrpcBody::Query(KrpcQuery::FindNode { id, target, .. }) => {
1924                assert_eq!(id.as_bytes(), b"abcdefghij0123456789");
1925                assert_eq!(target.as_bytes(), b"mnopqrstuvwxyz123456");
1926            }
1927            other => panic!("expected FindNode query, got {other:?}"),
1928        }
1929    }
1930
1931    #[test]
1932    fn krpc_decode_get_peers_query_spec() {
1933        // BEP 5 example: get_peers query
1934        let data = b"d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe";
1935        let msg = KrpcMessage::from_bytes(data).unwrap();
1936        assert_eq!(msg.transaction_id.0, *b"aa");
1937        assert!(!msg.read_only);
1938        match &msg.body {
1939            KrpcBody::Query(KrpcQuery::GetPeers {
1940                id,
1941                info_hash,
1942                noseed,
1943                scrape,
1944                ..
1945            }) => {
1946                assert_eq!(id.as_bytes(), b"abcdefghij0123456789");
1947                assert_eq!(info_hash.as_bytes(), b"mnopqrstuvwxyz123456");
1948                assert_eq!(*noseed, None);
1949                assert_eq!(*scrape, None);
1950            }
1951            other => panic!("expected GetPeers query, got {other:?}"),
1952        }
1953    }
1954
1955    #[test]
1956    fn krpc_decode_announce_peer_query_spec() {
1957        // BEP 5 example: announce_peer query
1958        let data = b"d1:ad2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoaborahe1:q13:announce_peer1:t2:aa1:y1:qe";
1959        let msg = KrpcMessage::from_bytes(data).unwrap();
1960        assert_eq!(msg.transaction_id.0, *b"aa");
1961        assert!(!msg.read_only);
1962        match &msg.body {
1963            KrpcBody::Query(KrpcQuery::AnnouncePeer {
1964                id,
1965                info_hash,
1966                port,
1967                implied_port,
1968                token,
1969            }) => {
1970                assert_eq!(id.as_bytes(), b"abcdefghij0123456789");
1971                assert_eq!(info_hash.as_bytes(), b"mnopqrstuvwxyz123456");
1972                assert_eq!(*port, 6881);
1973                assert!(*implied_port);
1974                assert_eq!(token, b"aoaborah");
1975            }
1976            other => panic!("expected AnnouncePeer query, got {other:?}"),
1977        }
1978    }
1979
1980    // ---- M241 L5: announce_peer port range-checking ----
1981
1982    fn m241_announce_args(
1983        port: Option<i64>,
1984        implied: Option<i64>,
1985    ) -> BTreeMap<Vec<u8>, BencodeValue> {
1986        let mut args = BTreeMap::new();
1987        args.insert(b"id".to_vec(), BencodeValue::Bytes(vec![0u8; 20]));
1988        args.insert(b"info_hash".to_vec(), BencodeValue::Bytes(vec![1u8; 20]));
1989        if let Some(p) = port {
1990            args.insert(b"port".to_vec(), BencodeValue::Integer(p));
1991        }
1992        if let Some(i) = implied {
1993            args.insert(b"implied_port".to_vec(), BencodeValue::Integer(i));
1994        }
1995        args.insert(b"token".to_vec(), BencodeValue::Bytes(b"tok".to_vec()));
1996        args
1997    }
1998
1999    #[test]
2000    fn m241_announce_peer_rejects_port_above_u16() {
2001        // 70000 was silently truncated to 4464 before M241.
2002        let args = m241_announce_args(Some(70000), None);
2003        assert!(matches!(
2004            decode_query(b"announce_peer", &args),
2005            Err(Error::InvalidMessage(_))
2006        ));
2007    }
2008
2009    #[test]
2010    fn m241_announce_peer_rejects_negative_port() {
2011        // -1 was silently truncated to 65535 before M241.
2012        let args = m241_announce_args(Some(-1), None);
2013        assert!(matches!(
2014            decode_query(b"announce_peer", &args),
2015            Err(Error::InvalidMessage(_))
2016        ));
2017    }
2018
2019    #[test]
2020    fn m241_announce_peer_rejects_zero_port_without_implied() {
2021        let args = m241_announce_args(Some(0), None);
2022        assert!(matches!(
2023            decode_query(b"announce_peer", &args),
2024            Err(Error::InvalidMessage(_))
2025        ));
2026    }
2027
2028    #[test]
2029    fn m241_announce_peer_accepts_zero_port_with_implied() {
2030        // implied_port=1: the source port is authoritative and `port` is ignored
2031        // downstream, so port=0 must NOT be rejected.
2032        let args = m241_announce_args(Some(0), Some(1));
2033        assert!(matches!(
2034            decode_query(b"announce_peer", &args),
2035            Ok(KrpcQuery::AnnouncePeer {
2036                port: 0,
2037                implied_port: true,
2038                ..
2039            })
2040        ));
2041    }
2042
2043    #[test]
2044    fn m241_announce_peer_accepts_out_of_range_port_with_implied() {
2045        // F6: with implied_port set, an out-of-range `port` is ignored downstream
2046        // and must not reject the whole query (BEP 5 interop) — normalized to 0.
2047        let args = m241_announce_args(Some(70000), Some(1));
2048        assert!(matches!(
2049            decode_query(b"announce_peer", &args),
2050            Ok(KrpcQuery::AnnouncePeer {
2051                port: 0,
2052                implied_port: true,
2053                ..
2054            })
2055        ));
2056    }
2057
2058    #[test]
2059    fn m241_announce_peer_accepts_valid_port() {
2060        let args = m241_announce_args(Some(6881), None);
2061        assert!(matches!(
2062            decode_query(b"announce_peer", &args),
2063            Ok(KrpcQuery::AnnouncePeer { port: 6881, .. })
2064        ));
2065    }
2066
2067    #[test]
2068    fn krpc_encode_ping_matches_spec() {
2069        // BEP 5 example: ping query
2070        // d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe
2071        //
2072        // Our encoder should produce byte-identical output when read_only=false
2073        // and sender_ip=None, since BTreeMap ordering matches bencode key order
2074        // and no extra fields are added.
2075        let msg = KrpcMessage {
2076            transaction_id: TransactionId([b'a', b'a']),
2077            body: KrpcBody::Query(KrpcQuery::Ping {
2078                id: bep5_id(b"abcdefghij0123456789"),
2079            }),
2080            sender_ip: None,
2081            read_only: false,
2082        };
2083        let encoded = msg.to_bytes().unwrap();
2084        let expected = b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe";
2085        assert_eq!(
2086            encoded, expected,
2087            "encoded ping query should match BEP 5 spec bytes exactly"
2088        );
2089
2090        // Verify required fields survive decode
2091        let decoded = KrpcMessage::from_bytes(&encoded).unwrap();
2092        assert_eq!(decoded.transaction_id.0, *b"aa");
2093        match &decoded.body {
2094            KrpcBody::Query(KrpcQuery::Ping { id }) => {
2095                assert_eq!(id.as_bytes(), b"abcdefghij0123456789");
2096            }
2097            other => panic!("expected Ping query, got {other:?}"),
2098        }
2099    }
2100
2101    #[test]
2102    fn krpc_encode_roundtrip_all_types() {
2103        // Encode -> decode -> verify field equality for multiple message types.
2104        let node_id = bep5_id(b"abcdefghij0123456789");
2105        let target = bep5_id(b"mnopqrstuvwxyz123456");
2106
2107        // 1. Ping query
2108        let ping = KrpcMessage {
2109            transaction_id: TransactionId::from_u16(1),
2110            body: KrpcBody::Query(KrpcQuery::Ping { id: node_id }),
2111            sender_ip: None,
2112            read_only: false,
2113        };
2114        let ping_decoded = KrpcMessage::from_bytes(&ping.to_bytes().unwrap()).unwrap();
2115        assert_eq!(ping, ping_decoded, "ping round-trip mismatch");
2116
2117        // 2. FindNode query
2118        let find_node = KrpcMessage {
2119            transaction_id: TransactionId::from_u16(2),
2120            body: KrpcBody::Query(KrpcQuery::FindNode {
2121                id: node_id,
2122                target,
2123                want: None,
2124            }),
2125            sender_ip: None,
2126            read_only: false,
2127        };
2128        let find_node_decoded = KrpcMessage::from_bytes(&find_node.to_bytes().unwrap()).unwrap();
2129        assert_eq!(
2130            find_node, find_node_decoded,
2131            "find_node round-trip mismatch"
2132        );
2133
2134        // 3. GetPeers query
2135        let get_peers = KrpcMessage {
2136            transaction_id: TransactionId::from_u16(3),
2137            body: KrpcBody::Query(KrpcQuery::GetPeers {
2138                id: node_id,
2139                info_hash: target,
2140                noseed: None,
2141                scrape: None,
2142                want: None,
2143            }),
2144            sender_ip: None,
2145            read_only: false,
2146        };
2147        let get_peers_decoded = KrpcMessage::from_bytes(&get_peers.to_bytes().unwrap()).unwrap();
2148        assert_eq!(
2149            get_peers, get_peers_decoded,
2150            "get_peers round-trip mismatch"
2151        );
2152
2153        // 4. AnnouncePeer query
2154        let announce = KrpcMessage {
2155            transaction_id: TransactionId::from_u16(4),
2156            body: KrpcBody::Query(KrpcQuery::AnnouncePeer {
2157                id: node_id,
2158                info_hash: target,
2159                port: 6881,
2160                implied_port: true,
2161                token: b"tok123".to_vec(),
2162            }),
2163            sender_ip: None,
2164            read_only: false,
2165        };
2166        let announce_decoded = KrpcMessage::from_bytes(&announce.to_bytes().unwrap()).unwrap();
2167        assert_eq!(
2168            announce, announce_decoded,
2169            "announce_peer round-trip mismatch"
2170        );
2171        match &announce_decoded.body {
2172            KrpcBody::Query(KrpcQuery::AnnouncePeer {
2173                id,
2174                info_hash,
2175                port,
2176                implied_port,
2177                token,
2178            }) => {
2179                assert_eq!(*id, node_id);
2180                assert_eq!(*info_hash, target);
2181                assert_eq!(*port, 6881);
2182                assert!(*implied_port);
2183                assert_eq!(token, b"tok123");
2184            }
2185            other => panic!("expected AnnouncePeer query, got {other:?}"),
2186        }
2187
2188        // 5. Error
2189        let error = KrpcMessage {
2190            transaction_id: TransactionId::from_u16(5),
2191            body: KrpcBody::Error {
2192                code: 201,
2193                message: "A Generic Error Occurred".into(),
2194            },
2195            sender_ip: None,
2196            read_only: false,
2197        };
2198        let error_decoded = KrpcMessage::from_bytes(&error.to_bytes().unwrap()).unwrap();
2199        assert_eq!(error, error_decoded, "error round-trip mismatch");
2200
2201        // 6. Response (ping response — just a node ID)
2202        let response = KrpcMessage {
2203            transaction_id: TransactionId::from_u16(6),
2204            body: KrpcBody::Response(KrpcResponse::NodeId { id: node_id }),
2205            sender_ip: None,
2206            read_only: false,
2207        };
2208        let response_decoded = KrpcMessage::from_bytes(&response.to_bytes().unwrap()).unwrap();
2209        assert_eq!(response, response_decoded, "response round-trip mismatch");
2210        match &response_decoded.body {
2211            KrpcBody::Response(KrpcResponse::NodeId { id }) => {
2212                assert_eq!(*id, node_id);
2213            }
2214            other => panic!("expected NodeId response, got {other:?}"),
2215        }
2216    }
2217
2218    #[test]
2219    fn scrape_response_noseed_parsed() {
2220        let msg = KrpcMessage {
2221            transaction_id: TransactionId::from_u16(332),
2222            body: KrpcBody::Query(KrpcQuery::GetPeers {
2223                id: test_id(),
2224                info_hash: target_id(),
2225                noseed: Some(1),
2226                scrape: None,
2227                want: None,
2228            }),
2229            sender_ip: None,
2230            read_only: false,
2231        };
2232        let bytes = msg.to_bytes().unwrap();
2233        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
2234        assert_eq!(msg, decoded);
2235        match &decoded.body {
2236            KrpcBody::Query(KrpcQuery::GetPeers { noseed, scrape, .. }) => {
2237                assert_eq!(*noseed, Some(1));
2238                assert_eq!(*scrape, None);
2239            }
2240            other => panic!("expected GetPeers query, got {other:?}"),
2241        }
2242    }
2243
2244    // ── BEP 45 want field tests ──────────────────────────────────────
2245
2246    #[test]
2247    fn want_family_round_trip_bytes() {
2248        assert_eq!(WantFamily::from_bytes(b"n4"), Some(WantFamily::N4));
2249        assert_eq!(WantFamily::from_bytes(b"n6"), Some(WantFamily::N6));
2250        assert_eq!(WantFamily::from_bytes(b"n8"), None);
2251        assert_eq!(WantFamily::N4.as_bytes(), b"n4");
2252        assert_eq!(WantFamily::N6.as_bytes(), b"n6");
2253    }
2254
2255    #[test]
2256    fn find_node_want_round_trip() {
2257        let msg = KrpcMessage {
2258            transaction_id: TransactionId::from_u16(500),
2259            body: KrpcBody::Query(KrpcQuery::FindNode {
2260                id: test_id(),
2261                target: target_id(),
2262                want: Some(vec![WantFamily::N4, WantFamily::N6]),
2263            }),
2264            sender_ip: None,
2265            read_only: false,
2266        };
2267        let bytes = msg.to_bytes().unwrap();
2268        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
2269        assert_eq!(msg, decoded);
2270        match &decoded.body {
2271            KrpcBody::Query(KrpcQuery::FindNode { want, .. }) => {
2272                let w = want.as_ref().expect("want should be present");
2273                assert_eq!(w.len(), 2);
2274                assert_eq!(w[0], WantFamily::N4);
2275                assert_eq!(w[1], WantFamily::N6);
2276            }
2277            other => panic!("expected FindNode query, got {other:?}"),
2278        }
2279    }
2280
2281    #[test]
2282    fn get_peers_want_round_trip() {
2283        let msg = KrpcMessage {
2284            transaction_id: TransactionId::from_u16(501),
2285            body: KrpcBody::Query(KrpcQuery::GetPeers {
2286                id: test_id(),
2287                info_hash: target_id(),
2288                noseed: None,
2289                scrape: None,
2290                want: Some(vec![WantFamily::N6]),
2291            }),
2292            sender_ip: None,
2293            read_only: false,
2294        };
2295        let bytes = msg.to_bytes().unwrap();
2296        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
2297        assert_eq!(msg, decoded);
2298        match &decoded.body {
2299            KrpcBody::Query(KrpcQuery::GetPeers { want, .. }) => {
2300                let w = want.as_ref().expect("want should be present");
2301                assert_eq!(w, &[WantFamily::N6]);
2302            }
2303            other => panic!("expected GetPeers query, got {other:?}"),
2304        }
2305    }
2306
2307    #[test]
2308    fn find_node_want_none_omitted() {
2309        let msg = KrpcMessage {
2310            transaction_id: TransactionId::from_u16(502),
2311            body: KrpcBody::Query(KrpcQuery::FindNode {
2312                id: test_id(),
2313                target: target_id(),
2314                want: None,
2315            }),
2316            sender_ip: None,
2317            read_only: false,
2318        };
2319        let bytes = msg.to_bytes().unwrap();
2320        let decoded = KrpcMessage::from_bytes(&bytes).unwrap();
2321        match &decoded.body {
2322            KrpcBody::Query(KrpcQuery::FindNode { want, .. }) => {
2323                assert!(want.is_none(), "want should be omitted when None");
2324            }
2325            other => panic!("expected FindNode query, got {other:?}"),
2326        }
2327    }
2328
2329    #[test]
2330    fn want_unknown_family_filtered() {
2331        let mut args = BTreeMap::new();
2332        args.insert(
2333            b"want".to_vec(),
2334            BencodeValue::List(vec![
2335                BencodeValue::Bytes(b"n4".to_vec()),
2336                BencodeValue::Bytes(b"n9".to_vec()),
2337                BencodeValue::Bytes(b"n6".to_vec()),
2338            ]),
2339        );
2340        let want = decode_want(&args).expect("should parse known families");
2341        assert_eq!(want, vec![WantFamily::N4, WantFamily::N6]);
2342    }
2343}