568 lines
18 KiB
Rust
568 lines
18 KiB
Rust
use std::{
|
|
io::{Read, Write},
|
|
net::TcpListener,
|
|
thread,
|
|
};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn builds_announce_and_scrape_urls() {
|
|
let request = TrackerRequestModel {
|
|
announce_url: "https://tracker.example.org/announce".to_owned(),
|
|
info_hash: "0123456789abcdef0123456789abcdef01234567".to_owned(),
|
|
peer_id: "89abcdef0123456789abcdef0123456789abcdef".to_owned(),
|
|
port: 6881,
|
|
uploaded: 1,
|
|
downloaded: 2,
|
|
left: 3,
|
|
event: Some("started".to_owned()),
|
|
compact: true,
|
|
numwant: Some(50),
|
|
};
|
|
|
|
let announce = request.announce_url().expect("announce url should build");
|
|
let scrape = request.scrape_url().expect("scrape url should build");
|
|
|
|
assert!(announce.starts_with("https://tracker.example.org/announce?"));
|
|
assert!(announce.contains("info_hash=%"));
|
|
assert!(announce.contains("peer_id=%"));
|
|
assert!(announce.contains("event=started"));
|
|
assert!(scrape.starts_with("https://tracker.example.org/scrape?"));
|
|
assert!(scrape.contains("info_hash=%"));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_dict_announce_response_with_peer_metadata() {
|
|
let response = announce_bytes(
|
|
1800,
|
|
Some("tracker-01"),
|
|
vec![peer_dict(
|
|
"127.0.0.1",
|
|
6881,
|
|
Some("qBittorrent 4.6.5"),
|
|
true,
|
|
false,
|
|
Some([1_u8; 20]),
|
|
)],
|
|
None,
|
|
);
|
|
|
|
let parsed =
|
|
TrackerResponseModel::from_announce_bytes(&response).expect("should parse announce");
|
|
|
|
assert_eq!(parsed.peers.interval_sec, 1800);
|
|
assert_eq!(parsed.peers.tracker_id.as_deref(), Some("tracker-01"));
|
|
assert_eq!(parsed.peers.peers.len(), 1);
|
|
assert_eq!(parsed.peers.peers[0].ip, "127.0.0.1");
|
|
assert_eq!(parsed.peers.peers[0].port, 6881);
|
|
assert_eq!(
|
|
parsed.peers.peers[0].client_name.as_deref(),
|
|
Some("qBittorrent 4.6.5")
|
|
);
|
|
assert!(parsed.peers.peers[0].choked);
|
|
assert!(!parsed.peers.peers[0].interested);
|
|
assert_eq!(parsed.peers.peers[0].peer_id, Some([1_u8; 20]));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_tracker_id_and_scrape_metadata_from_announce_response() {
|
|
let response = announce_bytes(
|
|
900,
|
|
Some("tracker-02"),
|
|
vec![peer_dict("127.0.0.2", 6882, None, false, true, None)],
|
|
Some((7, 3, 11)),
|
|
);
|
|
|
|
let parsed =
|
|
TrackerResponseModel::from_announce_bytes(&response).expect("should parse announce");
|
|
|
|
assert_eq!(parsed.peers.tracker_id.as_deref(), Some("tracker-02"));
|
|
assert_eq!(parsed.peers.interval_sec, 900);
|
|
let scrape = parsed.scrape.expect("scrape metadata should be present");
|
|
assert_eq!(scrape.complete, Some(7));
|
|
assert_eq!(scrape.incomplete, Some(3));
|
|
assert_eq!(scrape.downloaded, Some(11));
|
|
assert!(scrape.files.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_scrape_response_with_binary_info_hash_keys() {
|
|
let info_hash_a = [0x11_u8; 20];
|
|
let info_hash_b = [0x22_u8; 20];
|
|
let response = bencode_dict(vec![(
|
|
"files".to_owned(),
|
|
bencode_binary_key_dict(vec![
|
|
(
|
|
info_hash_a.to_vec(),
|
|
bencode_dict(vec![
|
|
("complete".to_owned(), bencode_int(7)),
|
|
("downloaded".to_owned(), bencode_int(9)),
|
|
("incomplete".to_owned(), bencode_int(3)),
|
|
]),
|
|
),
|
|
(
|
|
info_hash_b.to_vec(),
|
|
bencode_dict(vec![
|
|
("complete".to_owned(), bencode_int(4)),
|
|
("downloaded".to_owned(), bencode_int(5)),
|
|
("incomplete".to_owned(), bencode_int(6)),
|
|
]),
|
|
),
|
|
]),
|
|
)]);
|
|
|
|
let parsed = TrackerResponseModel::from_scrape_bytes(&response).expect("should parse scrape");
|
|
|
|
assert_eq!(parsed.files.len(), 2);
|
|
assert_eq!(parsed.files[0].info_hash, hex_encode(&info_hash_a));
|
|
assert_eq!(parsed.files[0].complete, Some(7));
|
|
assert_eq!(parsed.files[0].downloaded, Some(9));
|
|
assert_eq!(parsed.files[0].incomplete, Some(3));
|
|
assert_eq!(parsed.files[1].info_hash, hex_encode(&info_hash_b));
|
|
assert_eq!(parsed.files[1].complete, Some(4));
|
|
assert_eq!(parsed.files[1].downloaded, Some(5));
|
|
assert_eq!(parsed.files[1].incomplete, Some(6));
|
|
}
|
|
|
|
#[test]
|
|
fn udp_connect_request_serializes_expected_wire_format() {
|
|
let request = UdpTrackerConnectRequest {
|
|
transaction_id: UdpTrackerTransactionId::new(0x1020_3040),
|
|
};
|
|
|
|
assert_eq!(
|
|
request.encode(),
|
|
vec![
|
|
0x00, 0x00, 0x04, 0x17, 0x27, 0x10, 0x19, 0x80, 0x00, 0x00, 0x00, 0x00, 0x10, 0x20,
|
|
0x30, 0x40,
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn udp_connect_response_parses_header_and_connection_id() {
|
|
let response = [
|
|
0x00, 0x00, 0x00, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
0x08,
|
|
];
|
|
|
|
let parsed =
|
|
UdpTrackerConnectResponse::decode(&response).expect("connect response should parse");
|
|
|
|
assert_eq!(
|
|
parsed.transaction_id,
|
|
UdpTrackerTransactionId::new(0xaabb_ccdd)
|
|
);
|
|
assert_eq!(parsed.connection_id, 0x0102_0304_0506_0708);
|
|
}
|
|
|
|
#[test]
|
|
fn udp_response_header_rejects_invalid_action_id() {
|
|
let error = UdpTrackerResponseHeader::decode(&[0x00, 0x00, 0x00, 0x09, 0xaa, 0xbb, 0xcc, 0xdd])
|
|
.expect_err("invalid action id should fail");
|
|
|
|
assert!(matches!(error, TrackerParseError::InvalidUdpAction(9)));
|
|
}
|
|
|
|
#[test]
|
|
fn udp_response_header_rejects_truncated_payload() {
|
|
let error = UdpTrackerResponseHeader::decode(&[0x00, 0x00, 0x00])
|
|
.expect_err("truncated header should fail");
|
|
|
|
assert!(matches!(error, TrackerParseError::InvalidUdpPacket(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn udp_response_header_rejects_transaction_id_mismatch() {
|
|
let header = UdpTrackerResponseHeader {
|
|
action: UdpTrackerAction::Announce,
|
|
transaction_id: UdpTrackerTransactionId::new(7),
|
|
};
|
|
|
|
let error = header
|
|
.expect_transaction_id(UdpTrackerTransactionId::new(8))
|
|
.expect_err("mismatched transaction id should fail");
|
|
|
|
assert!(matches!(
|
|
error,
|
|
TrackerParseError::TransactionIdMismatch {
|
|
expected: 8,
|
|
actual: 7
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn udp_announce_request_serializes_expected_wire_format() {
|
|
let request = UdpTrackerAnnounceRequest {
|
|
connection_id: 0x0102_0304_0506_0708,
|
|
transaction_id: UdpTrackerTransactionId::new(0x5566_7788),
|
|
info_hash: [0x11_u8; 20],
|
|
peer_id: [0x22_u8; 20],
|
|
downloaded: 0x1122_3344_5566_7788,
|
|
left: 0x8877_6655_4433_2211,
|
|
uploaded: 0x0101_0202_0303_0404,
|
|
event: UdpTrackerAnnounceEvent::Started,
|
|
ip_address: 0,
|
|
key: 0x1234_5678,
|
|
numwant: -1,
|
|
port: 6881,
|
|
};
|
|
|
|
let encoded = request.encode();
|
|
|
|
assert_eq!(encoded.len(), 98);
|
|
assert_eq!(
|
|
&encoded[0..8],
|
|
&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
|
|
);
|
|
assert_eq!(&encoded[8..12], &[0x00, 0x00, 0x00, 0x01]);
|
|
assert_eq!(&encoded[12..16], &[0x55, 0x66, 0x77, 0x88]);
|
|
assert_eq!(&encoded[16..36], &[0x11_u8; 20]);
|
|
assert_eq!(&encoded[36..56], &[0x22_u8; 20]);
|
|
assert_eq!(&encoded[80..84], &[0x00, 0x00, 0x00, 0x02]);
|
|
assert_eq!(&encoded[88..92], &[0x12, 0x34, 0x56, 0x78]);
|
|
assert_eq!(&encoded[92..96], &[0xff, 0xff, 0xff, 0xff]);
|
|
assert_eq!(&encoded[96..98], &[0x1a, 0xe1]);
|
|
}
|
|
|
|
#[test]
|
|
fn udp_announce_response_parses_interval_counts_and_peers() {
|
|
let response = udp_announce_response_bytes(
|
|
UdpTrackerTransactionId::new(0x0102_0304),
|
|
1800,
|
|
4,
|
|
9,
|
|
&[(192, 168, 1, 10, 6881), (10, 0, 0, 2, 51413)],
|
|
);
|
|
|
|
let parsed =
|
|
UdpTrackerAnnounceResponse::decode(&response).expect("announce response should parse");
|
|
|
|
assert_eq!(
|
|
parsed.transaction_id,
|
|
UdpTrackerTransactionId::new(0x0102_0304)
|
|
);
|
|
assert_eq!(parsed.interval_sec, 1800);
|
|
assert_eq!(parsed.leechers, 4);
|
|
assert_eq!(parsed.seeders, 9);
|
|
assert_eq!(parsed.peers.len(), 2);
|
|
assert_eq!(parsed.peers[0].ip, "192.168.1.10");
|
|
assert_eq!(parsed.peers[0].port, 6881);
|
|
assert_eq!(parsed.peers[1].ip, "10.0.0.2");
|
|
assert_eq!(parsed.peers[1].port, 51413);
|
|
}
|
|
|
|
#[test]
|
|
fn udp_announce_response_rejects_malformed_compact_peer_blob() {
|
|
let mut response = vec![
|
|
0x00, 0x00, 0x00, 0x01, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x07, 0x08, 0x00, 0x00, 0x00,
|
|
0x03, 0x00, 0x00, 0x00, 0x06,
|
|
];
|
|
response.extend_from_slice(&[127, 0, 0, 1, 0x1a]);
|
|
|
|
let error =
|
|
UdpTrackerAnnounceResponse::decode(&response).expect_err("malformed peers should fail");
|
|
|
|
assert!(matches!(error, TrackerParseError::InvalidPeer(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn udp_scrape_request_serializes_multiple_info_hashes() {
|
|
let request = UdpTrackerScrapeRequest {
|
|
connection_id: 0x1112_1314_1516_1718,
|
|
transaction_id: UdpTrackerTransactionId::new(0x99aa_bbcc),
|
|
info_hashes: vec![[0x44_u8; 20], [0x55_u8; 20]],
|
|
};
|
|
|
|
let encoded = request.encode().expect("scrape request should encode");
|
|
|
|
assert_eq!(encoded.len(), 56);
|
|
assert_eq!(
|
|
&encoded[0..8],
|
|
&[0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18]
|
|
);
|
|
assert_eq!(&encoded[8..12], &[0x00, 0x00, 0x00, 0x02]);
|
|
assert_eq!(&encoded[12..16], &[0x99, 0xaa, 0xbb, 0xcc]);
|
|
assert_eq!(&encoded[16..36], &[0x44_u8; 20]);
|
|
assert_eq!(&encoded[36..56], &[0x55_u8; 20]);
|
|
}
|
|
|
|
#[test]
|
|
fn udp_scrape_response_maps_multiple_entries_to_scrape_model() {
|
|
let response = udp_scrape_response_bytes(
|
|
UdpTrackerTransactionId::new(0x0bad_f00d),
|
|
&[(7, 9, 3), (4, 5, 6)],
|
|
);
|
|
let parsed = UdpTrackerScrapeResponse::decode(&response).expect("scrape response should parse");
|
|
let scrape = parsed
|
|
.to_scrape_model(&[[0x33_u8; 20], [0x44_u8; 20]])
|
|
.expect("scrape model should build");
|
|
|
|
assert_eq!(
|
|
parsed.transaction_id,
|
|
UdpTrackerTransactionId::new(0x0bad_f00d)
|
|
);
|
|
assert_eq!(scrape.files.len(), 2);
|
|
assert_eq!(scrape.files[0].info_hash, hex_encode(&[0x33_u8; 20]));
|
|
assert_eq!(scrape.files[0].complete, Some(7));
|
|
assert_eq!(scrape.files[0].downloaded, Some(9));
|
|
assert_eq!(scrape.files[0].incomplete, Some(3));
|
|
assert_eq!(scrape.files[1].info_hash, hex_encode(&[0x44_u8; 20]));
|
|
assert_eq!(scrape.files[1].complete, Some(4));
|
|
assert_eq!(scrape.files[1].downloaded, Some(5));
|
|
assert_eq!(scrape.files[1].incomplete, Some(6));
|
|
}
|
|
|
|
#[test]
|
|
fn udp_scrape_response_rejects_truncated_payload() {
|
|
let error = UdpTrackerScrapeResponse::decode(&[
|
|
0x00, 0x00, 0x00, 0x02, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00,
|
|
])
|
|
.expect_err("truncated scrape payload should fail");
|
|
|
|
assert!(matches!(error, TrackerParseError::InvalidUdpPacket(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn reqwest_tracker_transport_executes_live_announce_request() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("local listener should bind");
|
|
let addr = listener.local_addr().expect("local addr should exist");
|
|
let handle = thread::spawn(move || {
|
|
let (mut stream, _) = listener.accept().expect("client should connect");
|
|
let mut request = [0_u8; 2048];
|
|
let read = stream.read(&mut request).expect("request should read");
|
|
let request_text = String::from_utf8_lossy(&request[..read]);
|
|
assert!(request_text.starts_with("GET /announce?"));
|
|
assert!(request_text.contains("info_hash="));
|
|
assert!(request_text.contains("peer_id="));
|
|
assert!(request_text.contains("compact=1"));
|
|
assert!(request_text.contains("event=started"));
|
|
|
|
let payload = announce_bytes(
|
|
1200,
|
|
Some("live-tracker"),
|
|
vec![peer_dict(
|
|
"127.0.0.1",
|
|
6881,
|
|
Some("local-peer"),
|
|
false,
|
|
true,
|
|
None,
|
|
)],
|
|
Some((5, 2, 9)),
|
|
);
|
|
let response = format!(
|
|
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/plain\r\n\r\n",
|
|
payload.len()
|
|
);
|
|
stream
|
|
.write_all(response.as_bytes())
|
|
.expect("headers should write");
|
|
stream.write_all(&payload).expect("payload should write");
|
|
});
|
|
|
|
let transport = ReqwestTrackerTransport::new().expect("tracker transport should build");
|
|
let response = transport
|
|
.announce(&TrackerRequestModel {
|
|
announce_url: format!("http://{addr}/announce"),
|
|
info_hash: "0123456789abcdef0123456789abcdef01234567".to_owned(),
|
|
peer_id: "89abcdef0123456789abcdef0123456789abcdef".to_owned(),
|
|
port: 6881,
|
|
uploaded: 0,
|
|
downloaded: 0,
|
|
left: 1024,
|
|
event: Some("started".to_owned()),
|
|
compact: true,
|
|
numwant: Some(25),
|
|
})
|
|
.expect("announce should succeed");
|
|
|
|
assert_eq!(response.peers.interval_sec, 1200);
|
|
assert_eq!(response.peers.tracker_id.as_deref(), Some("live-tracker"));
|
|
assert_eq!(response.peers.peers.len(), 1);
|
|
assert_eq!(response.peers.peers[0].ip, "127.0.0.1");
|
|
let scrape = response.scrape.expect("scrape stats should be present");
|
|
assert_eq!(scrape.complete, Some(5));
|
|
assert_eq!(scrape.incomplete, Some(2));
|
|
assert_eq!(scrape.downloaded, Some(9));
|
|
|
|
handle.join().expect("server thread should join");
|
|
}
|
|
|
|
#[test]
|
|
fn dht_node_model_parses_and_formats_ipv4_and_ipv6_specs() {
|
|
let ipv4 = DhtNodeModel::from_spec("198.51.100.9:51413").expect("ipv4 should parse");
|
|
assert_eq!(ipv4.address, "198.51.100.9");
|
|
assert_eq!(ipv4.port, 51413);
|
|
assert_eq!(ipv4.to_spec(), "198.51.100.9:51413");
|
|
|
|
let ipv6 = DhtNodeModel::from_spec("[2001:db8::9]:6881").expect("ipv6 should parse");
|
|
assert_eq!(ipv6.address, "2001:db8::9");
|
|
assert_eq!(ipv6.port, 6881);
|
|
assert_eq!(ipv6.to_spec(), "[2001:db8::9]:6881");
|
|
}
|
|
|
|
#[test]
|
|
fn tracker_request_converts_into_udp_announce_request() {
|
|
let request = TrackerRequestModel {
|
|
announce_url: "udp://tracker.example.org:6969".to_owned(),
|
|
info_hash: "00112233445566778899aabbccddeeff00112233".to_owned(),
|
|
peer_id: "89abcdef0123456789abcdef0123456789abcdef".to_owned(),
|
|
port: 51413,
|
|
uploaded: 11,
|
|
downloaded: 22,
|
|
left: 33,
|
|
event: Some("started".to_owned()),
|
|
compact: true,
|
|
numwant: Some(40),
|
|
};
|
|
|
|
let udp = request
|
|
.to_udp_announce_request(0x1122_3344_5566_7788, UdpTrackerTransactionId::new(77))
|
|
.expect("tracker request should convert");
|
|
|
|
assert_eq!(udp.connection_id, 0x1122_3344_5566_7788);
|
|
assert_eq!(udp.transaction_id, UdpTrackerTransactionId::new(77));
|
|
assert_eq!(udp.info_hash[0], 0x00);
|
|
assert_eq!(udp.info_hash[19], 0x33);
|
|
assert_eq!(udp.peer_id[0], 0x89);
|
|
assert_eq!(udp.peer_id[19], 0xef);
|
|
assert_eq!(udp.event, UdpTrackerAnnounceEvent::Started);
|
|
assert_eq!(udp.numwant, 40);
|
|
assert_eq!(udp.port, 51413);
|
|
}
|
|
|
|
fn announce_bytes(
|
|
interval: i64,
|
|
tracker_id: Option<&str>,
|
|
peers: Vec<Vec<u8>>,
|
|
scrape: Option<(i64, i64, i64)>,
|
|
) -> Vec<u8> {
|
|
let mut fields = Vec::new();
|
|
fields.push(("interval".to_owned(), bencode_int(interval)));
|
|
if let Some(tracker_id) = tracker_id {
|
|
fields.push((
|
|
"tracker id".to_owned(),
|
|
bencode_bytes(tracker_id.as_bytes()),
|
|
));
|
|
}
|
|
fields.push(("peers".to_owned(), bencode_list(peers)));
|
|
if let Some((complete, incomplete, downloaded)) = scrape {
|
|
fields.push(("complete".to_owned(), bencode_int(complete)));
|
|
fields.push(("incomplete".to_owned(), bencode_int(incomplete)));
|
|
fields.push(("downloaded".to_owned(), bencode_int(downloaded)));
|
|
}
|
|
bencode_dict(fields)
|
|
}
|
|
|
|
fn peer_dict(
|
|
ip: &str,
|
|
port: i64,
|
|
client: Option<&str>,
|
|
choked: bool,
|
|
interested: bool,
|
|
peer_id: Option<[u8; 20]>,
|
|
) -> Vec<u8> {
|
|
let mut fields = vec![
|
|
("ip".to_owned(), bencode_bytes(ip.as_bytes())),
|
|
("port".to_owned(), bencode_int(port)),
|
|
(
|
|
"choked".to_owned(),
|
|
bencode_int(i64::from(u8::from(choked))),
|
|
),
|
|
(
|
|
"interested".to_owned(),
|
|
bencode_int(i64::from(u8::from(interested))),
|
|
),
|
|
];
|
|
if let Some(client) = client {
|
|
fields.push(("client".to_owned(), bencode_bytes(client.as_bytes())));
|
|
}
|
|
if let Some(peer_id) = peer_id {
|
|
fields.push(("peer id".to_owned(), bencode_bytes(&peer_id)));
|
|
}
|
|
bencode_dict(fields)
|
|
}
|
|
|
|
fn bencode_dict(fields: Vec<(String, Vec<u8>)>) -> Vec<u8> {
|
|
let mut out = Vec::from(b"d".as_slice());
|
|
for (key, value) in fields {
|
|
out.extend_from_slice(key.len().to_string().as_bytes());
|
|
out.push(b':');
|
|
out.extend_from_slice(key.as_bytes());
|
|
out.extend_from_slice(&value);
|
|
}
|
|
out.push(b'e');
|
|
out
|
|
}
|
|
|
|
fn bencode_binary_key_dict(fields: Vec<(Vec<u8>, Vec<u8>)>) -> Vec<u8> {
|
|
let mut out = Vec::from(b"d".as_slice());
|
|
for (key, value) in fields {
|
|
out.extend_from_slice(key.len().to_string().as_bytes());
|
|
out.push(b':');
|
|
out.extend_from_slice(&key);
|
|
out.extend_from_slice(&value);
|
|
}
|
|
out.push(b'e');
|
|
out
|
|
}
|
|
|
|
fn bencode_list(values: Vec<Vec<u8>>) -> Vec<u8> {
|
|
let mut out = Vec::from(b"l".as_slice());
|
|
for value in values {
|
|
out.extend_from_slice(&value);
|
|
}
|
|
out.push(b'e');
|
|
out
|
|
}
|
|
|
|
fn bencode_int(value: i64) -> Vec<u8> {
|
|
format!("i{value}e").into_bytes()
|
|
}
|
|
|
|
fn bencode_bytes(value: &[u8]) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
out.extend_from_slice(value.len().to_string().as_bytes());
|
|
out.push(b':');
|
|
out.extend_from_slice(value);
|
|
out
|
|
}
|
|
|
|
fn udp_announce_response_bytes(
|
|
transaction_id: UdpTrackerTransactionId,
|
|
interval_sec: u32,
|
|
leechers: u32,
|
|
seeders: u32,
|
|
peers: &[(u8, u8, u8, u8, u16)],
|
|
) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
out.extend_from_slice(&1_u32.to_be_bytes());
|
|
out.extend_from_slice(&transaction_id.get().to_be_bytes());
|
|
out.extend_from_slice(&interval_sec.to_be_bytes());
|
|
out.extend_from_slice(&leechers.to_be_bytes());
|
|
out.extend_from_slice(&seeders.to_be_bytes());
|
|
for (a, b, c, d, port) in peers {
|
|
out.extend_from_slice(&[*a, *b, *c, *d]);
|
|
out.extend_from_slice(&port.to_be_bytes());
|
|
}
|
|
out
|
|
}
|
|
|
|
fn udp_scrape_response_bytes(
|
|
transaction_id: UdpTrackerTransactionId,
|
|
entries: &[(u32, u32, u32)],
|
|
) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
out.extend_from_slice(&2_u32.to_be_bytes());
|
|
out.extend_from_slice(&transaction_id.get().to_be_bytes());
|
|
for (complete, downloaded, incomplete) in entries {
|
|
out.extend_from_slice(&complete.to_be_bytes());
|
|
out.extend_from_slice(&downloaded.to_be_bytes());
|
|
out.extend_from_slice(&incomplete.to_be_bytes());
|
|
}
|
|
out
|
|
}
|