chore: initial sanitized public snapshot

This commit is contained in:
Aria2 Rust Pro Contributors
2026-07-18 16:01:12 +08:00
commit 7c6b6a3746
321 changed files with 76896 additions and 0 deletions
@@ -0,0 +1,303 @@
use super::{DhtMessageModel, TorrentPeerModel, TransportError};
use crate::tracker::{
TrackerParseError, UdpTrackerAnnounceEvent, UdpTrackerAnnounceRequest, UdpTrackerTransactionId,
};
/// Announce request fields sent to an HTTP or UDP tracker.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrackerRequestModel {
/// Base announce URL.
pub announce_url: String,
/// Hex-encoded torrent info hash.
pub info_hash: String,
/// Hex-encoded local peer identifier.
pub peer_id: String,
/// Listening port exposed to peers.
pub port: u16,
/// Uploaded byte counter sent to the tracker.
pub uploaded: u64,
/// Downloaded byte counter sent to the tracker.
pub downloaded: u64,
/// Remaining byte counter sent to the tracker.
pub left: u64,
/// Optional tracker lifecycle event.
pub event: Option<String>,
/// Whether the tracker should prefer the compact peer format.
pub compact: bool,
/// Optional requested peer count.
pub numwant: Option<u32>,
}
/// Peer list returned by a tracker announce response.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrackerPeerListModel {
/// Recommended announce interval in seconds.
pub interval_sec: u32,
/// Parsed peer entries.
pub peers: Vec<TorrentPeerModel>,
/// Optional minimum announce interval in seconds.
pub min_interval_sec: Option<u32>,
/// Optional tracker session identifier.
pub tracker_id: Option<String>,
}
/// DHT node coordinate returned by tracker or DHT metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DhtNodeModel {
/// Hex-encoded node identifier when available.
pub node_id: String,
/// Node IP address or hostname.
pub address: String,
/// Node UDP port.
pub port: u16,
}
impl DhtNodeModel {
/// Parses a DHT bootstrap node from a `host:port` or `[ipv6]:port` spec.
///
/// # Errors
///
/// Returns an error when the node spec is malformed or the port is invalid.
pub fn from_spec(raw: &str) -> Result<Self, TrackerParseError> {
let (address, port) = super::parsing::parse_endpoint_spec(raw, "dht node")?;
Ok(Self {
node_id: String::new(),
address,
port,
})
}
/// Formats the node as a stable `host:port` or `[ipv6]:port` spec.
#[must_use]
pub fn to_spec(&self) -> String {
super::parsing::format_endpoint_spec(&self.address, self.port)
}
}
/// Scrape statistics for a single info hash.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrackerScrapeFileModel {
/// Hex-encoded info hash the entry describes.
pub info_hash: String,
/// Number of completed downloads.
pub complete: Option<u32>,
/// Number of times the torrent was downloaded.
pub downloaded: Option<u32>,
/// Number of incomplete peers.
pub incomplete: Option<u32>,
}
/// Scrape summary returned by a tracker.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrackerScrapeModel {
/// Aggregate completed download count when present.
pub complete: Option<u32>,
/// Aggregate download count when present.
pub downloaded: Option<u32>,
/// Aggregate incomplete peer count when present.
pub incomplete: Option<u32>,
/// Per-info-hash scrape entries.
pub files: Vec<TrackerScrapeFileModel>,
}
/// Parsed tracker response containing announce peers and optional scrape data.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrackerResponseModel {
/// Peer-list payload from the announce response.
pub peers: TrackerPeerListModel,
/// Optional scrape metadata synthesized from the response.
pub scrape: Option<TrackerScrapeModel>,
}
impl TrackerRequestModel {
/// Builds a tracker request from raw `BitTorrent` info-hash and peer-id bytes.
#[must_use]
pub fn from_bt_bytes(
announce_url: impl Into<String>,
info_hash: [u8; 20],
peer_id: [u8; 20],
port: u16,
uploaded: u64,
downloaded: u64,
left: u64,
) -> Self {
Self {
announce_url: announce_url.into(),
info_hash: super::parsing::hex_encode(&info_hash),
peer_id: super::parsing::hex_encode(&peer_id),
port,
uploaded,
downloaded,
left,
event: None,
compact: true,
numwant: Some(50),
}
}
/// Returns sorted query pairs suitable for announce and scrape URLs.
#[must_use]
pub fn query_pairs(&self) -> Vec<(String, String)> {
let mut pairs = vec![
("info_hash".to_owned(), self.info_hash.clone()),
("peer_id".to_owned(), self.peer_id.clone()),
("port".to_owned(), self.port.to_string()),
("uploaded".to_owned(), self.uploaded.to_string()),
("downloaded".to_owned(), self.downloaded.to_string()),
("left".to_owned(), self.left.to_string()),
("compact".to_owned(), u8::from(self.compact).to_string()),
];
if let Some(event) = &self.event {
pairs.push(("event".to_owned(), event.clone()));
}
if let Some(numwant) = self.numwant {
pairs.push(("numwant".to_owned(), numwant.to_string()));
}
pairs
}
/// Builds the full announce URL with query parameters.
///
/// # Errors
///
/// Returns an error when the announce URL is invalid.
pub fn announce_url(&self) -> Result<String, TrackerParseError> {
super::parsing::build_url(&self.announce_url, &self.query_pairs())
}
/// Builds the matching scrape URL with query parameters.
///
/// # Errors
///
/// Returns an error when the derived scrape URL is invalid.
pub fn scrape_url(&self) -> Result<String, TrackerParseError> {
let base = super::parsing::tracker_scrape_url(&self.announce_url);
super::parsing::build_url(&base, &self.query_pairs())
}
/// Decodes the hex-encoded info hash into its raw 20-byte form.
///
/// # Errors
///
/// Returns an error when the info hash is not a valid 20-byte hex string.
pub fn info_hash_bytes(&self) -> Result<[u8; 20], TrackerParseError> {
super::parsing::decode_hex_20(&self.info_hash)
}
/// Decodes the hex-encoded peer id into its raw 20-byte form.
///
/// # Errors
///
/// Returns an error when the peer id is not a valid 20-byte hex string.
pub fn peer_id_bytes(&self) -> Result<[u8; 20], TrackerParseError> {
super::parsing::decode_hex_20(&self.peer_id)
}
/// Converts the higher-level request into a UDP tracker announce request.
///
/// # Errors
///
/// Returns an error when the info hash, peer id, event, or numwant cannot be represented
/// in a UDP announce packet.
pub fn to_udp_announce_request(
&self,
connection_id: u64,
transaction_id: UdpTrackerTransactionId,
) -> Result<UdpTrackerAnnounceRequest, TrackerParseError> {
let event = match self.event.as_deref() {
None | Some("") => UdpTrackerAnnounceEvent::None,
Some("completed") => UdpTrackerAnnounceEvent::Completed,
Some("started") => UdpTrackerAnnounceEvent::Started,
Some("stopped") => UdpTrackerAnnounceEvent::Stopped,
Some(other) => {
return Err(TrackerParseError::InvalidUdpPacket(format!(
"unsupported udp tracker event: {other}"
)));
}
};
let numwant = match self.numwant {
Some(numwant) => i32::try_from(numwant).map_err(|_| {
TrackerParseError::InvalidUdpPacket(format!(
"udp tracker numwant exceeds i32 range: {numwant}"
))
})?,
None => -1,
};
Ok(UdpTrackerAnnounceRequest {
connection_id,
transaction_id,
info_hash: self.info_hash_bytes()?,
peer_id: self.peer_id_bytes()?,
downloaded: self.downloaded,
left: self.left,
uploaded: self.uploaded,
event,
ip_address: 0,
key: 0,
numwant,
port: self.port,
})
}
}
impl TrackerResponseModel {
/// Parses an HTTP tracker announce payload.
///
/// # Errors
///
/// Returns an error when the payload is malformed bencode or lacks required fields.
pub fn from_announce_bytes(input: &[u8]) -> Result<Self, TrackerParseError> {
let root = super::parsing::parse_bencode(input)?;
let interval_sec = super::parsing::i64_to_u32(
super::parsing::dict_get_int(&root, "interval").unwrap_or(1800),
);
let min_interval_sec =
super::parsing::dict_get_int(&root, "min interval").map(super::parsing::i64_to_u32);
let tracker_id = super::parsing::dict_get_bytes(&root, "tracker id")
.map(super::parsing::bytes_to_string);
let peers = super::parsing::parse_peer_list(&root)?;
let scrape = super::parsing::parse_scrape_section(&root);
Ok(Self {
peers: TrackerPeerListModel {
interval_sec,
peers,
min_interval_sec,
tracker_id,
},
scrape,
})
}
/// Parses an HTTP tracker scrape payload.
///
/// # Errors
///
/// Returns an error when the payload is malformed or lacks scrape metadata.
pub fn from_scrape_bytes(input: &[u8]) -> Result<TrackerScrapeModel, TrackerParseError> {
let root = super::parsing::parse_bencode(input)?;
super::parsing::parse_scrape_section_from_root(&root)
.ok_or(TrackerParseError::MissingField("files"))
}
}
/// Tracker transport contract for announce and scrape requests.
pub trait TrackerTransport {
/// Executes a tracker announce request.
fn announce(
&self,
request: &TrackerRequestModel,
) -> Result<TrackerResponseModel, TransportError>;
/// Executes a tracker scrape request for the given announce URL.
fn scrape(&self, announce_url: &str) -> Result<TrackerScrapeModel, TransportError>;
}
/// DHT transport contract for request/response messaging.
pub trait DhtTransport {
/// Sends a DHT message to the target node and returns the response.
fn send_message(
&self,
node: &DhtNodeModel,
message: &DhtMessageModel,
) -> Result<DhtMessageModel, TransportError>;
}