use std::collections::BTreeMap; use super::{ DhtAnnouncePeerQueryModel, DhtCompactNodeModel, DhtFindNodeResponseModel, DhtGetPeersResponseModel, DhtMessageBody, DhtMessageModel, DhtQueryModel, DhtResponseModel, PeerWireBitfieldModel, PeerWireBlockRequestModel, PeerWireExtensionHandshakeModel, PeerWireExtensionMessageModel, PeerWireFrameHeaderModel, PeerWireHandshakeModel, PeerWireMessageKind, PeerWireMessageModel, PeerWireMetadataMessageModel, PeerWireMetadataMessageType, PeerWirePieceBlockModel, PeerWireUnknownMessageModel, TorrentMessageModel, decode_compact_dht_nodes, encode_compact_dht_nodes, parse_torrent_bootstrap, parse_torrent_metadata, }; #[test] fn dht_ping_query_roundtrip_serializes_and_parses() { let message = DhtMessageModel::ping_query(b"aa".to_vec(), vec![0x11; 20]); assert_eq!(message.method(), Some("ping")); assert!(message.is_query()); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded).expect("encoded ping should parse"); assert_eq!(decoded, message); assert_eq!(decoded.transaction_id(), b"aa"); } #[test] fn dht_ping_response_roundtrip_serializes_and_parses() { let message = DhtMessageModel::ping_response(b"pr".to_vec(), vec![0x44; 20]); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded).expect("encoded ping should parse"); assert_eq!(decoded, message); assert!(matches!( decoded.body, DhtMessageBody::Response(DhtResponseModel::Ping(_)) )); } #[test] fn dht_find_node_query_roundtrip_serializes_and_parses() { let message = DhtMessageModel::find_node_query(b"fn".to_vec(), vec![0x22; 20], vec![0x33; 20]); assert_eq!(message.method(), Some("find_node")); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded).expect("encoded find_node should parse"); assert_eq!(decoded, message); } #[test] fn dht_announce_peer_query_roundtrip_serializes_and_parses() { let message = DhtMessageModel::announce_peer_query( b"ap".to_vec(), vec![0x11; 20], vec![0x22; 20], 6881, b"tok".to_vec(), true, ); assert_eq!(message.method(), Some("announce_peer")); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded).expect("encoded announce_peer should parse"); assert_eq!(decoded, message); assert!(matches!( decoded.body, DhtMessageBody::Query(DhtQueryModel::AnnouncePeer(DhtAnnouncePeerQueryModel { implied_port: true, port: 6881, .. })) )); } #[test] fn dht_get_peers_query_roundtrip_serializes_and_parses() { let message = DhtMessageModel::get_peers_query(b"gp".to_vec(), vec![0x22; 20], vec![0x33; 20]); assert_eq!(message.method(), Some("get_peers")); assert!(matches!( &message.body, DhtMessageBody::Query(DhtQueryModel::GetPeers(_)) )); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded).expect("encoded get_peers should parse"); assert_eq!(decoded, message); } #[test] fn dht_get_peers_response_roundtrip_preserves_nodes_values_and_token() { let message = DhtMessageModel::get_peers_response( b"r1".to_vec(), vec![0x44; 20], Some(b"tok".to_vec()), Some(vec![0xaa, 0xbb, 0xcc, 0xdd]), vec![vec![127, 0, 0, 1, 0x1a, 0xe1]], ); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded).expect("encoded response should parse"); assert_eq!(decoded, message); assert!(matches!( decoded.body, DhtMessageBody::Response(DhtResponseModel::GetPeers(DhtGetPeersResponseModel { token: Some(_), nodes: Some(_), .. })) )); } #[test] fn dht_find_node_response_roundtrip_preserves_compact_nodes() { let nodes = vec![ DhtCompactNodeModel { node_id: [0x11; 20], address: [127, 0, 0, 1], port: 6881, }, DhtCompactNodeModel { node_id: [0x22; 20], address: [192, 0, 2, 1], port: 51413, }, ]; let message = DhtMessageModel::find_node_response(b"fnr".to_vec(), vec![0x33; 20], nodes); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded) .expect("encoded find_node response should parse"); assert_eq!(decoded, message); assert!(matches!( decoded.body, DhtMessageBody::Response(DhtResponseModel::FindNode(DhtFindNodeResponseModel { nodes: ref parsed_nodes, .. })) if parsed_nodes.len() == 2 )); } #[test] fn compact_dht_node_codec_roundtrip_serializes_bytes_in_network_order() { let node = DhtCompactNodeModel { node_id: [0x7f; 20], address: [198, 51, 100, 7], port: 51413, }; let bytes = encode_compact_dht_nodes(std::slice::from_ref(&node)); assert_eq!(bytes.len(), 26); let decoded = decode_compact_dht_nodes(&bytes).expect("compact node codec should parse"); assert_eq!(decoded, vec![node]); } #[test] fn dht_error_roundtrip_serializes_and_parses() { let message = DhtMessageModel::error_response(b"e1".to_vec(), 203, "protocol error"); let encoded = message.to_bencode_bytes(); let decoded = DhtMessageModel::from_bencode_bytes(&encoded).expect("encoded error should parse"); assert_eq!(decoded, message); assert_eq!(decoded.method(), None); } #[test] fn dht_parse_rejects_unsupported_query_method() { let payload = b"d1:ad2:id20:aaaaaaaaaaaaaaaaaaaae1:q4:find1:t2:aa1:y1:qe"; let error = DhtMessageModel::from_bencode_bytes(payload).expect_err("unsupported method should fail"); assert!(error.contains("unsupported dht query method")); } #[test] fn parses_single_file_torrent_metadata() { let torrent = br"d8:announce35:http://tracker.example.org/announce4:infod4:name10:ubuntu.iso12:piece lengthi16384e6:lengthi32768e6:pieces40:aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbee"; let metadata = parse_torrent_metadata(torrent).expect("torrent metadata should parse"); assert_eq!(metadata.info.name, "ubuntu.iso"); assert_eq!(metadata.info.piece_length, 16384); assert_eq!(metadata.info.files.len(), 1); assert_eq!( metadata.announce.as_deref(), Some("http://tracker.example.org/announce") ); assert_eq!(metadata.trackers.len(), 1); assert_eq!(metadata.pieces.len(), 2); assert_eq!( metadata .info .hash .as_ref() .map(|hash| hash.info_hash_hex.len()), Some(40) ); } #[test] fn parses_announce_list_tiers_with_stable_tier_indices() { let torrent = br"d8:announce35:http://tracker.example.org/announce13:announce-listll30:udp://tier1-a.example.org:696935:http://tier1-b.example.org/announceel30:udp://tier2-a.example.org:6969ee4:infod6:lengthi4096e4:name8:mini.iso12:piece lengthi1024e6:pieces20:aaaaaaaaaaaaaaaaaaaaee"; let metadata = parse_torrent_metadata(torrent).expect("torrent metadata should parse"); assert_eq!(metadata.trackers.len(), 4); assert_eq!( metadata.trackers[0].url, "http://tracker.example.org/announce" ); assert_eq!(metadata.trackers[0].tier, Some(0)); assert_eq!(metadata.trackers[1].url, "udp://tier1-a.example.org:6969"); assert_eq!(metadata.trackers[1].tier, Some(1)); assert_eq!( metadata.trackers[2].url, "http://tier1-b.example.org/announce" ); assert_eq!(metadata.trackers[2].tier, Some(1)); assert_eq!(metadata.trackers[3].url, "udp://tier2-a.example.org:6969"); assert_eq!(metadata.trackers[3].tier, Some(2)); } #[test] fn parses_multi_file_paths_and_piece_offsets() { let torrent = br"d8:announce35:http://tracker.example.org/announce4:infod5:filesld6:lengthi123e4:pathl4:dir110:file-a.bineed6:lengthi200e4:pathl4:dir24:subd10:file-b.bineed6:lengthi45e4:pathl10:readme.txteee4:name6:bundle12:piece lengthi128e6:pieces60:aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbccccccccccccccccccccee"; let metadata = parse_torrent_metadata(torrent).expect("multi-file torrent should parse"); assert_eq!(metadata.info.files.len(), 3); assert_eq!(metadata.info.files[0].path, "dir1/file-a.bin"); assert_eq!(metadata.info.files[0].length, 123); assert_eq!(metadata.info.files[0].piece_offset, Some(0)); assert_eq!(metadata.info.files[1].path, "dir2/subd/file-b.bin"); assert_eq!(metadata.info.files[1].length, 200); assert_eq!(metadata.info.files[1].piece_offset, Some(123)); assert_eq!(metadata.info.files[2].path, "readme.txt"); assert_eq!(metadata.info.files[2].length, 45); assert_eq!(metadata.info.files[2].piece_offset, Some(323)); assert_eq!(metadata.total_length(), 368); } #[test] fn parses_dht_nodes_from_nodes_list_when_present() { let torrent = br"d8:announce35:http://tracker.example.org/announce5:nodesll17:router.bittorrenti6881eel14:node.local.lani51413eee4:infod6:lengthi2048e4:name8:node.iso12:piece lengthi1024e6:pieces20:aaaaaaaaaaaaaaaaaaaaee"; let metadata = parse_torrent_metadata(torrent).expect("torrent with nodes should parse"); assert_eq!(metadata.dht_nodes.len(), 2); assert_eq!(metadata.dht_nodes[0], "router.bittorrent:6881"); assert_eq!(metadata.dht_nodes[1], "node.local.lan:51413"); } #[test] fn torrent_bootstrap_exposes_magnet_info_hash_and_dht_models() { let torrent = br"d8:announce35:http://tracker.example.org/announce13:announce-listll30:udp://tier1-a.example.org:696935:http://tier1-b.example.org/announceee5:nodesll17:router.bittorrenti6881eel14:node.local.lani51413eee4:infod6:lengthi2048e4:name8:node.iso12:piece lengthi1024e6:pieces40:aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbee"; let bootstrap = parse_torrent_bootstrap(torrent).expect("torrent bootstrap should parse"); assert_eq!(bootstrap.metadata.info.name, "node.iso"); assert_eq!( bootstrap.info_hash_hex, bootstrap .metadata .info .hash .as_ref() .expect("torrent hash should exist") .info_hash_hex ); assert_eq!(bootstrap.info_hash_bytes.len(), 20); assert_eq!( bootstrap.magnet.trackers, vec![ "http://tracker.example.org/announce".to_owned(), "udp://tier1-a.example.org:6969".to_owned(), "http://tier1-b.example.org/announce".to_owned(), ] ); assert_eq!(bootstrap.dht_nodes[0].to_spec(), "router.bittorrent:6881"); assert_eq!(bootstrap.dht_nodes[1].to_spec(), "node.local.lan:51413"); } #[test] fn dht_get_peers_response_helpers_decode_peer_values_and_nodes() { let response = DhtMessageModel::get_peers_response( b"gp".to_vec(), vec![0x11; 20], Some(b"tok".to_vec()), Some(vec![ 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0, 0x00, 0x01, 0x02, 0x03, 0x04, 192, 0, 2, 10, 0x1a, 0xe1, ]), vec![vec![198, 51, 100, 9, 0xc8, 0xd5]], ); let DhtMessageBody::Response(DhtResponseModel::GetPeers(payload)) = response.body else { panic!("expected get_peers response"); }; let peers = payload .peer_contacts() .expect("compact peers should decode"); assert_eq!(peers.len(), 1); assert_eq!(peers[0].ip, "198.51.100.9"); assert_eq!(peers[0].port, 51413); let nodes = payload.dht_nodes().expect("compact nodes should decode"); assert_eq!(nodes.len(), 1); assert_eq!(nodes[0].node_id, "102030405060708090a0b0c0d0e0f00001020304"); assert_eq!(nodes[0].to_spec(), "192.0.2.10:6881"); } #[test] fn peer_wire_handshake_roundtrip_preserves_reserved_bits_and_ids() { let handshake = PeerWireHandshakeModel { reserved: [0, 0, 0, 0, 0, 0x10, 0, 0x01], info_hash: [0x11; 20], peer_id: [0x22; 20], }; let bytes = handshake.serialize(); let parsed = PeerWireHandshakeModel::parse(&bytes).expect("handshake bytes should parse cleanly"); assert_eq!(parsed, handshake); assert!(parsed.extension_protocol_enabled()); assert!(parsed.dht_enabled()); } #[test] fn peer_wire_handshake_rejects_truncated_and_invalid_protocol_bytes() { let truncated = vec![19, b'B', b'i']; assert!( PeerWireHandshakeModel::parse(&truncated) .expect_err("truncated handshake should fail") .contains("truncated") ); let mut invalid = PeerWireHandshakeModel { reserved: [0; 8], info_hash: [1; 20], peer_id: [2; 20], } .serialize(); invalid[1] = b'X'; assert!( PeerWireHandshakeModel::parse(&invalid) .expect_err("invalid protocol name should fail") .contains("protocol") ); } #[test] fn peer_wire_keepalive_roundtrip_uses_zero_length_frame() { let message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::KeepAlive); let bytes = message .serialize_peer_wire_frame() .expect("keepalive should serialize"); assert_eq!(bytes, vec![0, 0, 0, 0]); let parsed = TorrentMessageModel::parse_peer_wire_frame_exact(&bytes).expect("keepalive should parse"); assert_eq!(parsed, message); assert_eq!(parsed.peer_wire_kind(), Ok(PeerWireMessageKind::KeepAlive)); } #[test] fn peer_wire_inspect_frame_reports_message_id_payload_len_and_consumed_bytes() { let keepalive_header = TorrentMessageModel::inspect_peer_wire_frame(&[0, 0, 0, 0]) .expect("keepalive frame header should parse"); assert_eq!( keepalive_header, ( PeerWireFrameHeaderModel { message_id: None, payload_len: 0, }, 4 ) ); let request = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Request( PeerWireBlockRequestModel { piece_index: 1, block_offset: 2, block_length: 16_384, }, )); let mut request_frame = request .serialize_peer_wire_frame() .expect("request frame should serialize"); request_frame.extend_from_slice(&[0x99, 0x88, 0x77]); let request_header = TorrentMessageModel::inspect_peer_wire_frame(&request_frame) .expect("request frame header should parse"); assert_eq!( request_header, ( PeerWireFrameHeaderModel { message_id: Some(6), payload_len: 12, }, 17 ) ); } #[test] fn peer_wire_inspect_frame_rejects_truncated_prefix_or_payload() { assert!( TorrentMessageModel::inspect_peer_wire_frame(&[0, 0, 0]) .expect_err("missing length prefix should fail") .contains("missing length prefix") ); assert!( TorrentMessageModel::inspect_peer_wire_frame(&[0, 0, 0, 1]) .expect_err("missing message id should fail") .contains("truncated") ); } #[test] fn peer_wire_control_messages_roundtrip_through_wrapper_surface() { let cases = [ PeerWireMessageKind::Choke, PeerWireMessageKind::Unchoke, PeerWireMessageKind::Interested, PeerWireMessageKind::NotInterested, ]; for kind in cases { let wire = PeerWireMessageModel::new( Some([0x44; 20]), TorrentMessageModel::from_peer_wire_kind(kind.clone()), ); let bytes = wire .serialize_frame() .expect("control peer-wire frame should serialize"); let parsed = PeerWireMessageModel::parse_frame_exact(&bytes) .expect("control peer-wire frame should parse"); assert_eq!(parsed.peer_id, None); assert_eq!(parsed.message.peer_wire_kind(), Ok(kind)); } } #[test] fn peer_wire_bitfield_helpers_pack_bits_msb_first() { let flags = [ true, false, true, true, false, false, false, true, true, false, ]; let bitfield = PeerWireBitfieldModel::from_piece_flags(&flags); assert_eq!(bitfield.bytes, vec![0b1011_0001, 0b1000_0000]); assert_eq!(bitfield.piece_capacity(), 16); assert!(bitfield.has_piece(0)); assert!(bitfield.has_piece(8)); assert!(!bitfield.has_piece(9)); assert_eq!(bitfield.to_piece_flags(flags.len()), flags); let message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Bitfield(bitfield.clone())); let roundtrip = TorrentMessageModel::parse_peer_wire_frame_exact( &message .serialize_peer_wire_frame() .expect("bitfield should serialize"), ) .expect("bitfield should roundtrip"); assert_eq!( roundtrip.peer_wire_kind(), Ok(PeerWireMessageKind::Bitfield(bitfield)) ); } #[test] fn peer_wire_have_request_and_cancel_roundtrip() { let have = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Have(77)); let request = PeerWireBlockRequestModel { piece_index: 7, block_offset: 16_384, block_length: 4_096, }; let request_message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Request(request)); let cancel_message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Cancel(request)); assert_eq!( TorrentMessageModel::parse_peer_wire_frame_exact( &have .serialize_peer_wire_frame() .expect("have should serialize"), ) .expect("have should parse") .peer_wire_kind(), Ok(PeerWireMessageKind::Have(77)) ); assert_eq!( TorrentMessageModel::parse_peer_wire_frame_exact( &request_message .serialize_peer_wire_frame() .expect("request should serialize"), ) .expect("request should parse") .peer_wire_kind(), Ok(PeerWireMessageKind::Request(request)) ); assert_eq!( TorrentMessageModel::parse_peer_wire_frame_exact( &cancel_message .serialize_peer_wire_frame() .expect("cancel should serialize"), ) .expect("cancel should parse") .peer_wire_kind(), Ok(PeerWireMessageKind::Cancel(request)) ); } #[test] fn peer_wire_piece_port_extension_and_unknown_roundtrip() { let piece = PeerWirePieceBlockModel { piece_index: 5, block_offset: 32_768, block: b"block-data".to_vec(), }; let extension = PeerWireExtensionMessageModel { extension_message_id: 3, payload: b"ut_metadata".to_vec(), }; let unknown = PeerWireUnknownMessageModel { message_id: 99, payload: vec![9, 8, 7, 6], }; let piece_message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Piece(piece.clone())); let port_message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Port(51413)); let extension_message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Extension(extension.clone())); let unknown_message = TorrentMessageModel::from_peer_wire_kind(PeerWireMessageKind::Unknown(unknown.clone())); assert_eq!( TorrentMessageModel::parse_peer_wire_frame_exact( &piece_message .serialize_peer_wire_frame() .expect("piece should serialize"), ) .expect("piece should parse") .peer_wire_kind(), Ok(PeerWireMessageKind::Piece(piece)) ); assert_eq!( TorrentMessageModel::parse_peer_wire_frame_exact( &port_message .serialize_peer_wire_frame() .expect("port should serialize"), ) .expect("port should parse") .peer_wire_kind(), Ok(PeerWireMessageKind::Port(51413)) ); assert_eq!( TorrentMessageModel::parse_peer_wire_frame_exact( &extension_message .serialize_peer_wire_frame() .expect("extension should serialize"), ) .expect("extension should parse") .peer_wire_kind(), Ok(PeerWireMessageKind::Extension(extension)) ); assert_eq!( TorrentMessageModel::parse_peer_wire_frame_exact( &unknown_message .serialize_peer_wire_frame() .expect("unknown should serialize"), ) .expect("unknown should parse") .peer_wire_kind(), Ok(PeerWireMessageKind::Unknown(unknown)) ); } #[test] fn extension_handshake_and_ut_metadata_messages_roundtrip() { let handshake = PeerWireExtensionHandshakeModel { extensions: BTreeMap::from([ ("ut_metadata".to_owned(), 3_u8), ("ut_pex".to_owned(), 1_u8), ]), client_name: Some("aria2-rust-pro".to_owned()), metadata_size: Some(32_768), request_queue: Some(32), }; let handshake_message = handshake.to_peer_wire_message(); assert_eq!(handshake_message.extension_message_id, 0); let parsed_handshake = PeerWireExtensionHandshakeModel::from_peer_wire_message(&handshake_message) .expect("extended handshake should parse"); assert_eq!(parsed_handshake, handshake); assert_eq!(parsed_handshake.ut_metadata_id(), Some(3)); assert_eq!(parsed_handshake.metadata_piece_count(), Some(2)); let request = PeerWireMetadataMessageModel::request(7); let parsed_request = PeerWireMetadataMessageModel::from_peer_wire_message(&request.to_peer_wire_message(3), 3) .expect("metadata request should parse"); assert_eq!( parsed_request.message_type, PeerWireMetadataMessageType::Request ); assert_eq!(parsed_request.piece, 7); assert!(parsed_request.payload.is_empty()); let data = PeerWireMetadataMessageModel::data(0, 11, b"piece-bytes".to_vec()); let parsed_data = PeerWireMetadataMessageModel::from_peer_wire_message(&data.to_peer_wire_message(3), 3) .expect("metadata data should parse"); assert_eq!(parsed_data.message_type, PeerWireMetadataMessageType::Data); assert_eq!(parsed_data.piece, 0); assert_eq!(parsed_data.total_size, Some(11)); assert_eq!(parsed_data.payload, b"piece-bytes"); let reject = PeerWireMetadataMessageModel::reject(4); let parsed_reject = PeerWireMetadataMessageModel::from_peer_wire_message(&reject.to_peer_wire_message(3), 3) .expect("metadata reject should parse"); assert_eq!( parsed_reject.message_type, PeerWireMetadataMessageType::Reject ); assert_eq!(parsed_reject.piece, 4); } #[test] fn extended_handshake_treats_zero_ut_metadata_id_as_disabled() { let handshake = PeerWireExtensionHandshakeModel { extensions: BTreeMap::from([ ("ut_metadata".to_owned(), 0_u8), ("ut_pex".to_owned(), 1_u8), ]), client_name: Some("aria2-rust-pro".to_owned()), metadata_size: Some(16_384), request_queue: Some(8), }; let parsed = PeerWireExtensionHandshakeModel::from_bencode_bytes(&handshake.to_bencode_bytes()) .expect("extended handshake should parse"); assert_eq!(parsed.extensions.get("ut_metadata"), Some(&0)); assert_eq!(parsed.ut_metadata_id(), None); assert_eq!(parsed.metadata_piece_count(), Some(1)); } #[test] fn ut_metadata_data_messages_reject_out_of_range_piece_indexes() { let invalid = b"d8:msg_typei1e5:piecei2e10:total_sizei16384eepayload".to_vec(); assert!( PeerWireMetadataMessageModel::from_bencode_bytes(&invalid) .expect_err("piece index beyond metadata size should fail") .contains("piece") ); } #[test] fn ut_metadata_data_messages_reject_payload_lengths_that_do_not_match_piece_geometry() { let invalid = b"d8:msg_typei1e5:piecei0e10:total_sizei16385ee".to_vec(); assert!( PeerWireMetadataMessageModel::from_bencode_bytes(&invalid) .expect_err("empty payload for non-empty piece should fail") .contains("payload") ); } #[test] fn ut_metadata_request_and_reject_messages_reject_total_size_fields() { let request_with_total_size = b"d8:msg_typei0e5:piecei0e10:total_sizei16384ee".to_vec(); assert!( PeerWireMetadataMessageModel::from_bencode_bytes(&request_with_total_size) .expect_err("request total_size should fail") .contains("total_size") ); let reject_with_total_size = b"d8:msg_typei2e5:piecei1e10:total_sizei16384ee".to_vec(); assert!( PeerWireMetadataMessageModel::from_bencode_bytes(&reject_with_total_size) .expect_err("reject total_size should fail") .contains("total_size") ); } #[test] fn peer_wire_parse_rejects_truncated_and_invalid_payload_shapes() { let truncated = [0, 0, 0, 13, 6, 0, 0, 0, 1, 0, 0]; assert!( TorrentMessageModel::parse_peer_wire_frame_exact(&truncated) .expect_err("truncated request frame should fail") .contains("truncated") ); let invalid_have = [0, 0, 0, 4, 4, 0, 0, 0]; assert!( TorrentMessageModel::parse_peer_wire_frame_exact(&invalid_have) .expect_err("invalid have frame should fail") .contains("have") ); let invalid_extension = [0, 0, 0, 1, 20]; assert!( TorrentMessageModel::parse_peer_wire_frame_exact(&invalid_extension) .expect_err("extension frame missing ext id should fail") .contains("extension") ); }