chore: initial sanitized public snapshot

This commit is contained in:
Aria2 Rust Pro Contributors
2026-07-18 14:04:26 +08:00
commit 7b3816441c
320 changed files with 76813 additions and 0 deletions
@@ -0,0 +1,490 @@
use super::{AtomicU32, Ordering, TorrentPeerModel};
use crate::tracker::{
TrackerParseError, TrackerPeerListModel, TrackerResponseModel, TrackerScrapeFileModel,
TrackerScrapeModel,
};
/// UDP tracker protocol identifier from BEP 15.
pub const UDP_TRACKER_PROTOCOL_ID: u64 = 0x0417_2710_1980;
/// Process-local counter used to allocate monotonic UDP tracker transaction ids.
static UDP_TRACKER_TRANSACTION_COUNTER: AtomicU32 = AtomicU32::new(0x6d69_0000);
/// Monotonic transaction identifier used for UDP tracker requests.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UdpTrackerTransactionId(u32);
impl UdpTrackerTransactionId {
/// Creates a transaction identifier from a raw integer value.
#[must_use]
pub const fn new(value: u32) -> Self {
Self(value)
}
/// Allocates the next process-local UDP tracker transaction identifier.
#[must_use]
pub fn next() -> Self {
Self(UDP_TRACKER_TRANSACTION_COUNTER.fetch_add(1, Ordering::Relaxed))
}
/// Returns the raw integer value sent on the wire.
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
/// UDP tracker actions defined by BEP 15.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UdpTrackerAction {
/// Connection-id bootstrap request/response.
Connect,
/// Announce request/response.
Announce,
/// Scrape request/response.
Scrape,
/// Error response.
Error,
}
impl UdpTrackerAction {
/// Returns the wire value associated with the action.
#[must_use]
pub const fn wire_value(self) -> u32 {
match self {
Self::Connect => 0,
Self::Announce => 1,
Self::Scrape => 2,
Self::Error => 3,
}
}
/// Decodes a raw BEP 15 action code into the typed tracker action.
fn decode(value: u32) -> Result<Self, TrackerParseError> {
match value {
0 => Ok(Self::Connect),
1 => Ok(Self::Announce),
2 => Ok(Self::Scrape),
3 => Ok(Self::Error),
_ => Err(TrackerParseError::InvalidUdpAction(value)),
}
}
#[must_use]
/// Returns a short diagnostic label for the tracker action.
const fn label(self) -> &'static str {
match self {
Self::Connect => "connect",
Self::Announce => "announce",
Self::Scrape => "scrape",
Self::Error => "error",
}
}
}
/// Announce lifecycle values defined by the UDP tracker protocol.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UdpTrackerAnnounceEvent {
/// No explicit lifecycle event.
None,
/// Download completed.
Completed,
/// Download started.
Started,
/// Download stopped.
Stopped,
}
impl UdpTrackerAnnounceEvent {
/// Returns the wire value associated with the announce event.
#[must_use]
pub const fn wire_value(self) -> u32 {
match self {
Self::None => 0,
Self::Completed => 1,
Self::Started => 2,
Self::Stopped => 3,
}
}
}
/// Header shared by all UDP tracker responses.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UdpTrackerResponseHeader {
/// Action encoded by the response.
pub action: UdpTrackerAction,
/// Transaction identifier associated with the request/response pair.
pub transaction_id: UdpTrackerTransactionId,
}
impl UdpTrackerResponseHeader {
/// Decodes a UDP tracker response header from raw bytes.
///
/// # Errors
///
/// Returns an error when the payload is truncated or the action code is invalid.
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
ensure_udp_payload_len(input, 8, "tracker response header")?;
Ok(Self {
action: UdpTrackerAction::decode(read_u32_be(input, 0, "tracker action id")?)?,
transaction_id: UdpTrackerTransactionId::new(read_u32_be(
input,
4,
"tracker transaction id",
)?),
})
}
/// Verifies that the header action matches the expected value.
///
/// # Errors
///
/// Returns an error when the action differs from `expected`.
pub fn expect_action(self, expected: UdpTrackerAction) -> Result<Self, TrackerParseError> {
if self.action == expected {
Ok(self)
} else {
Err(TrackerParseError::InvalidUdpPacket(format!(
"expected tracker action {} but got {}",
expected.label(),
self.action.label()
)))
}
}
/// Verifies that the header transaction identifier matches the expected value.
///
/// # Errors
///
/// Returns an error when the transaction identifier differs from `expected`.
pub fn expect_transaction_id(
self,
expected: UdpTrackerTransactionId,
) -> Result<Self, TrackerParseError> {
if self.transaction_id == expected {
Ok(self)
} else {
Err(TrackerParseError::TransactionIdMismatch {
expected: expected.get(),
actual: self.transaction_id.get(),
})
}
}
}
/// UDP tracker connect request payload.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UdpTrackerConnectRequest {
/// Transaction identifier to match in the response.
pub transaction_id: UdpTrackerTransactionId,
}
impl UdpTrackerConnectRequest {
/// Encodes the request into BEP 15 wire bytes.
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(16);
out.extend_from_slice(&UDP_TRACKER_PROTOCOL_ID.to_be_bytes());
out.extend_from_slice(&UdpTrackerAction::Connect.wire_value().to_be_bytes());
out.extend_from_slice(&self.transaction_id.get().to_be_bytes());
out
}
}
/// UDP tracker connect response payload.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UdpTrackerConnectResponse {
/// Transaction identifier echoed by the tracker.
pub transaction_id: UdpTrackerTransactionId,
/// Connection identifier used by later requests.
pub connection_id: u64,
}
impl UdpTrackerConnectResponse {
/// Decodes a connect response from BEP 15 wire bytes.
///
/// # Errors
///
/// Returns an error when the payload is truncated or malformed.
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
ensure_udp_payload_len(input, 16, "connect response")?;
let header =
UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Connect)?;
Ok(Self {
transaction_id: header.transaction_id,
connection_id: read_u64_be(input, 8, "tracker connection id")?,
})
}
}
/// UDP tracker announce request payload.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UdpTrackerAnnounceRequest {
/// Connection identifier previously returned by the tracker.
pub connection_id: u64,
/// Transaction identifier to match in the response.
pub transaction_id: UdpTrackerTransactionId,
/// Raw 20-byte torrent info hash.
pub info_hash: [u8; 20],
/// Raw 20-byte local peer identifier.
pub peer_id: [u8; 20],
/// Uploaded byte count.
pub downloaded: u64,
/// Remaining byte count.
pub left: u64,
/// Uploaded byte count.
pub uploaded: u64,
/// Announce lifecycle event.
pub event: UdpTrackerAnnounceEvent,
/// Optional explicit IPv4 address encoded as a `u32`.
pub ip_address: u32,
/// Opaque tracker key.
pub key: u32,
/// Desired peer count or `-1` for tracker default.
pub numwant: i32,
/// Listening port exposed to peers.
pub port: u16,
}
impl UdpTrackerAnnounceRequest {
/// Encodes the announce request into BEP 15 wire bytes.
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(98);
out.extend_from_slice(&self.connection_id.to_be_bytes());
out.extend_from_slice(&UdpTrackerAction::Announce.wire_value().to_be_bytes());
out.extend_from_slice(&self.transaction_id.get().to_be_bytes());
out.extend_from_slice(&self.info_hash);
out.extend_from_slice(&self.peer_id);
out.extend_from_slice(&self.downloaded.to_be_bytes());
out.extend_from_slice(&self.left.to_be_bytes());
out.extend_from_slice(&self.uploaded.to_be_bytes());
out.extend_from_slice(&self.event.wire_value().to_be_bytes());
out.extend_from_slice(&self.ip_address.to_be_bytes());
out.extend_from_slice(&self.key.to_be_bytes());
out.extend_from_slice(&self.numwant.to_be_bytes());
out.extend_from_slice(&self.port.to_be_bytes());
out
}
}
/// UDP tracker announce response payload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UdpTrackerAnnounceResponse {
/// Transaction identifier echoed by the tracker.
pub transaction_id: UdpTrackerTransactionId,
/// Recommended announce interval in seconds.
pub interval_sec: u32,
/// Number of incomplete peers.
pub leechers: u32,
/// Number of complete peers.
pub seeders: u32,
/// Parsed compact IPv4 peer list.
pub peers: Vec<TorrentPeerModel>,
}
impl UdpTrackerAnnounceResponse {
/// Decodes an announce response from BEP 15 wire bytes.
///
/// # Errors
///
/// Returns an error when the payload is truncated or malformed.
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
ensure_udp_payload_len(input, 20, "announce response")?;
let header =
UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Announce)?;
Ok(Self {
transaction_id: header.transaction_id,
interval_sec: read_u32_be(input, 8, "announce interval")?,
leechers: read_u32_be(input, 12, "announce leechers")?,
seeders: read_u32_be(input, 16, "announce seeders")?,
peers: super::parsing::parse_compact_peers_ipv4(&input[20..])?,
})
}
/// Converts the announce response into the higher-level tracker response model.
#[must_use]
pub fn into_tracker_response(self) -> TrackerResponseModel {
TrackerResponseModel {
peers: TrackerPeerListModel {
interval_sec: self.interval_sec,
peers: self.peers,
min_interval_sec: None,
tracker_id: None,
},
scrape: None,
}
}
}
/// UDP tracker scrape request payload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UdpTrackerScrapeRequest {
/// Connection identifier previously returned by the tracker.
pub connection_id: u64,
/// Transaction identifier to match in the response.
pub transaction_id: UdpTrackerTransactionId,
/// Raw 20-byte info hashes to scrape.
pub info_hashes: Vec<[u8; 20]>,
}
impl UdpTrackerScrapeRequest {
/// Encodes the scrape request into BEP 15 wire bytes.
///
/// # Errors
///
/// Returns an error when no info hashes were supplied.
pub fn encode(&self) -> Result<Vec<u8>, TrackerParseError> {
if self.info_hashes.is_empty() {
return Err(TrackerParseError::InvalidUdpPacket(
"udp tracker scrape request requires at least one info hash".to_owned(),
));
}
let mut out = Vec::with_capacity(16 + self.info_hashes.len() * 20);
out.extend_from_slice(&self.connection_id.to_be_bytes());
out.extend_from_slice(&UdpTrackerAction::Scrape.wire_value().to_be_bytes());
out.extend_from_slice(&self.transaction_id.get().to_be_bytes());
for info_hash in &self.info_hashes {
out.extend_from_slice(info_hash);
}
Ok(out)
}
}
/// Per-torrent counters returned by a UDP tracker scrape response.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct UdpTrackerScrapeStats {
/// Number of completed downloads.
pub complete: u32,
/// Number of completed client downloads.
pub downloaded: u32,
/// Number of incomplete peers.
pub incomplete: u32,
}
/// UDP tracker scrape response payload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UdpTrackerScrapeResponse {
/// Transaction identifier echoed by the tracker.
pub transaction_id: UdpTrackerTransactionId,
/// Per-info-hash scrape statistics.
pub files: Vec<UdpTrackerScrapeStats>,
}
impl UdpTrackerScrapeResponse {
/// Decodes a scrape response from BEP 15 wire bytes.
///
/// # Errors
///
/// Returns an error when the payload is truncated or malformed.
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
ensure_udp_payload_len(input, 8, "scrape response header")?;
let header =
UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Scrape)?;
let payload = &input[8..];
if !payload.len().is_multiple_of(12) {
return Err(TrackerParseError::InvalidUdpPacket(
"scrape response payload length must be divisible by 12".to_owned(),
));
}
let mut files = Vec::with_capacity(payload.len() / 12);
for offset in (0..payload.len()).step_by(12) {
files.push(UdpTrackerScrapeStats {
complete: read_u32_be(payload, offset, "scrape complete count")?,
downloaded: read_u32_be(payload, offset + 4, "scrape downloaded count")?,
incomplete: read_u32_be(payload, offset + 8, "scrape incomplete count")?,
});
}
Ok(Self {
transaction_id: header.transaction_id,
files,
})
}
/// Converts the response into the higher-level scrape model.
///
/// # Errors
///
/// Returns an error when the info-hash list does not match the response entry count.
pub fn to_scrape_model(
&self,
info_hashes: &[[u8; 20]],
) -> Result<TrackerScrapeModel, TrackerParseError> {
if info_hashes.len() != self.files.len() {
return Err(TrackerParseError::InvalidUdpPacket(format!(
"scrape response entry count {} does not match info-hash count {}",
self.files.len(),
info_hashes.len()
)));
}
let files = info_hashes
.iter()
.zip(self.files.iter())
.map(|(info_hash, stats)| TrackerScrapeFileModel {
info_hash: super::parsing::hex_encode(info_hash),
complete: Some(stats.complete),
downloaded: Some(stats.downloaded),
incomplete: Some(stats.incomplete),
})
.collect::<Vec<_>>();
let (complete, downloaded, incomplete) = if self.files.len() == 1 {
let stats = self.files[0];
(
Some(stats.complete),
Some(stats.downloaded),
Some(stats.incomplete),
)
} else {
(None, None, None)
};
Ok(TrackerScrapeModel {
complete,
downloaded,
incomplete,
files,
})
}
}
/// Verifies that a UDP tracker payload is at least `min_len` bytes long.
fn ensure_udp_payload_len(
input: &[u8],
min_len: usize,
label: &str,
) -> Result<(), TrackerParseError> {
if input.len() < min_len {
return Err(TrackerParseError::InvalidUdpPacket(format!(
"{label} truncated: expected at least {min_len} bytes but got {}",
input.len()
)));
}
Ok(())
}
/// Reads one big-endian `u32` from a UDP tracker payload.
fn read_u32_be(input: &[u8], offset: usize, field: &str) -> Result<u32, TrackerParseError> {
let end = offset.saturating_add(4);
let bytes = input
.get(offset..end)
.ok_or_else(|| TrackerParseError::InvalidUdpPacket(format!("{field} truncated")))?;
let mut out = [0_u8; 4];
out.copy_from_slice(bytes);
Ok(u32::from_be_bytes(out))
}
/// Reads one big-endian `u64` from a UDP tracker payload.
fn read_u64_be(input: &[u8], offset: usize, field: &str) -> Result<u64, TrackerParseError> {
let end = offset.saturating_add(8);
let bytes = input
.get(offset..end)
.ok_or_else(|| TrackerParseError::InvalidUdpPacket(format!("{field} truncated")))?;
let mut out = [0_u8; 8];
out.copy_from_slice(bytes);
Ok(u64::from_be_bytes(out))
}