Files
aria2-rust-pro/crates/aria2-rust-pro-rpc/src/dispatcher/compat_support/bt_runtime.rs
T

363 lines
12 KiB
Rust

use super::{
BTreeMap, BtFileInfo, BtPeerInfo, BtRuntimeState, BtTrackerInfo, DhtNodeModel, Digest,
MagnetBootstrapModel, MagnetUriModel, PieceId, PieceMap, PieceState, RequestGroup, RpcError,
TorrentMetadataModel, hex_string, merge_bt_dht_nodes, parse_torrent_metadata,
};
/// Extracts an uppercase BitTorrent info hash from a magnet URI.
pub(in crate::dispatcher) fn rpc_bt_info_hash(uri: &str) -> Option<String> {
let lower = uri.to_ascii_lowercase();
let marker = "xt=urn:btih:";
let start = lower.find(marker)?;
let raw = &uri[start + marker.len()..];
let token = raw.split('&').next().unwrap_or(raw);
let normalized: String = token
.chars()
.filter(|ch| ch.is_ascii_alphanumeric())
.map(|ch| ch.to_ascii_uppercase())
.collect();
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
/// Converts parsed torrent metadata into the core BitTorrent runtime model.
pub(in crate::dispatcher) fn build_bt_runtime_state(
metadata: &TorrentMetadataModel,
) -> BtRuntimeState {
let trackers: Vec<BtTrackerInfo> = metadata
.trackers
.iter()
.map(|tracker| BtTrackerInfo {
url: tracker.url.clone(),
tier: tracker.tier,
id: tracker.id.clone(),
seeders: tracker.seeders,
leechers: tracker.leechers,
})
.collect();
let files = metadata
.info
.files
.iter()
.map(|file| BtFileInfo {
path: file.path.clone(),
length: file.length,
piece_offset: file.piece_offset,
selected: file.selected,
})
.collect();
let peers = metadata
.peers
.iter()
.map(|peer| BtPeerInfo {
peer_id: peer.peer_id.map(|peer_id| hex_string(&peer_id)),
ip: peer.ip.clone(),
port: peer.port,
client_name: peer.client_name.clone(),
interested: peer.interested,
choked: peer.choked,
download_speed: 0,
upload_speed: 0,
seeder: false,
})
.collect();
let info_hash = metadata
.info
.hash
.as_ref()
.map(|hash| hash.info_hash_hex.to_ascii_uppercase())
.unwrap_or_default();
let magnet_uri = (!info_hash.is_empty()).then(|| {
MagnetUriModel {
info_hash: info_hash.clone(),
display_name: Some(metadata.info.name.clone()),
trackers: trackers.iter().map(|tracker| tracker.url.clone()).collect(),
web_seeds: Vec::new(),
exact_topic: None,
}
.to_uri()
});
BtRuntimeState {
info_hash,
name: Some(metadata.info.name.clone()),
magnet_uri,
metadata_only: false,
metadata_size: None,
metadata_extension_ids: BTreeMap::new(),
metadata_piece_payloads: BTreeMap::new(),
creation_date: metadata.creation_date.clone(),
comment: metadata.comment.clone(),
dht_nodes: initial_bt_dht_nodes(&metadata.dht_nodes),
files,
trackers,
peers,
}
}
/// Seeds BitTorrent runtime state from a magnet URI before metadata arrives.
pub(in crate::dispatcher) fn build_bt_runtime_state_from_magnet(
uri: &str,
magnet: &MagnetBootstrapModel,
) -> BtRuntimeState {
let magnet_uri = Some(uri.to_owned());
let trackers = magnet
.trackers
.iter()
.map(|tracker| BtTrackerInfo {
url: tracker.url.clone(),
tier: tracker.tier,
id: tracker.id.clone(),
seeders: tracker.seeders,
leechers: tracker.leechers,
})
.collect();
let peer_hints = magnet
.peer_hints
.iter()
.map(|peer| BtPeerInfo {
peer_id: peer
.peer_id
.map(|peer_id: [u8; 20]| hex_string(&peer_id[..])),
ip: peer.ip.clone(),
port: peer.port,
client_name: peer.client_name.clone(),
interested: peer.interested,
choked: peer.choked,
download_speed: 0,
upload_speed: 0,
seeder: false,
})
.collect();
let hinted_dht_nodes = magnet
.peer_hint_nodes
.iter()
.map(DhtNodeModel::to_spec)
.collect::<Vec<_>>();
BtRuntimeState {
info_hash: magnet.info_hash_hex.to_ascii_uppercase(),
name: magnet.uri.display_name.clone(),
magnet_uri,
metadata_only: true,
metadata_size: None,
metadata_extension_ids: BTreeMap::new(),
metadata_piece_payloads: BTreeMap::new(),
creation_date: None,
comment: None,
dht_nodes: initial_bt_dht_nodes(&hinted_dht_nodes),
files: Vec::new(),
trackers,
peers: peer_hints,
}
}
/// Returns the built-in fallback DHT router list.
pub(in crate::dispatcher) fn default_bt_dht_nodes() -> Vec<String> {
vec![
"router.bittorrent.com:6881".to_owned(),
"dht.transmissionbt.com:6881".to_owned(),
"router.utorrent.com:6881".to_owned(),
]
}
/// Chooses explicit DHT nodes when present and otherwise falls back to router defaults.
pub(in crate::dispatcher) fn initial_bt_dht_nodes(explicit_nodes: &[String]) -> Vec<String> {
if explicit_nodes.is_empty() {
default_bt_dht_nodes()
} else {
explicit_nodes.to_vec()
}
}
/// Returns the BEP 9 metadata piece size used by aria2-compatible peers.
pub(in crate::dispatcher) const BT_METADATA_PIECE_LENGTH: u64 = 16 * 1024;
/// Returns the number of metadata pieces needed for one BEP 9 payload size.
pub(in crate::dispatcher) fn bt_metadata_piece_count(metadata_size: u32) -> u32 {
if metadata_size == 0 {
return 0;
}
metadata_size.div_ceil(BT_METADATA_PIECE_LENGTH as u32)
}
/// Returns the byte length of one metadata piece within the BEP 9 payload.
pub(in crate::dispatcher) fn bt_metadata_piece_span(metadata_size: u32, piece_index: u32) -> usize {
let piece_start = u64::from(piece_index).saturating_mul(BT_METADATA_PIECE_LENGTH);
let metadata_size = u64::from(metadata_size);
if piece_start >= metadata_size {
return 0;
}
let remaining = metadata_size.saturating_sub(piece_start);
usize::try_from(remaining.min(BT_METADATA_PIECE_LENGTH)).unwrap_or(usize::MAX)
}
/// Returns a stable runtime key for one BT peer endpoint.
pub(in crate::dispatcher) fn bt_peer_metadata_key(peer: &BtPeerInfo) -> String {
format!("{}:{}", peer.ip, peer.port)
}
/// Merges BT tracker snapshots by URL while preserving any existing runtime rows.
pub(in crate::dispatcher) fn merge_bt_trackers(
existing: &mut Vec<BtTrackerInfo>,
incoming: Vec<BtTrackerInfo>,
) {
for tracker in incoming {
if existing.iter().any(|current| current.url == tracker.url) {
continue;
}
existing.push(tracker);
}
}
/// Merges BT peer snapshots while preserving existing runtime rows.
pub(in crate::dispatcher) fn merge_bt_peers(
existing: &mut Vec<BtPeerInfo>,
incoming: Vec<BtPeerInfo>,
) {
for peer in incoming {
if let Some(current) = existing.iter_mut().find(|candidate| {
candidate.peer_id.as_deref() == peer.peer_id.as_deref()
|| (candidate.ip == peer.ip && candidate.port == peer.port)
}) {
*current = peer;
} else {
existing.push(peer);
}
}
}
/// Promotes a metadata-only magnet runtime into a full torrent-backed BT session when ready.
pub(in crate::dispatcher) fn try_promote_bt_metadata(
group: &mut RequestGroup,
) -> Result<bool, RpcError> {
let Some(bt) = group.bt().cloned() else {
return Err(RpcError::unsupported(
"metadata promotion requires bt runtime state",
));
};
if !bt.metadata_only {
return Ok(false);
}
let Some(metadata_size) = bt.metadata_size.filter(|size| *size > 0) else {
return Ok(false);
};
let piece_count = bt_metadata_piece_count(metadata_size);
if piece_count == 0 {
return Ok(false);
}
let mut metadata_bytes =
Vec::with_capacity(usize::try_from(metadata_size).unwrap_or(usize::MAX));
for piece in 0..piece_count {
let Some(payload) = bt.metadata_piece_payloads.get(&piece) else {
return Ok(false);
};
let expected_len = bt_metadata_piece_span(metadata_size, piece);
if payload.len() < expected_len {
return Ok(false);
}
metadata_bytes.extend_from_slice(&payload[..expected_len]);
}
metadata_bytes.truncate(usize::try_from(metadata_size).unwrap_or(usize::MAX));
let metadata = parse_torrent_metadata(&metadata_bytes).map_err(|error| {
RpcError::unsupported(&format!("invalid magnet metadata payload: {error}"))
})?;
let parsed_info_hash = metadata
.info
.hash
.as_ref()
.map(|hash| hash.info_hash_hex.to_ascii_uppercase())
.unwrap_or_default();
if parsed_info_hash.is_empty() {
return Err(RpcError::unsupported(
"promoted torrent metadata did not expose an info hash",
));
}
if !bt.info_hash.is_empty() && !parsed_info_hash.eq_ignore_ascii_case(&bt.info_hash) {
return Err(RpcError::unsupported(&format!(
"magnet metadata info hash mismatch: expected {}, got {parsed_info_hash}",
bt.info_hash
)));
}
let mut promoted = build_bt_runtime_state(&metadata);
promoted.magnet_uri = bt.magnet_uri.clone();
promoted.metadata_size = Some(metadata_size);
promoted.metadata_extension_ids = bt.metadata_extension_ids.clone();
promoted.metadata_piece_payloads = bt.metadata_piece_payloads.clone();
promoted.dht_nodes = bt.dht_nodes.clone();
merge_bt_dht_nodes(&mut promoted.dht_nodes, metadata.dht_nodes.iter().cloned());
merge_bt_trackers(&mut promoted.trackers, bt.trackers.clone());
let metadata_peers = std::mem::take(&mut promoted.peers);
promoted.peers = bt.peers.clone();
merge_bt_peers(&mut promoted.peers, metadata_peers);
group.set_bt(promoted);
group.set_total_length(metadata.total_length());
group.set_piece_length(metadata.info.piece_length.max(1));
group.set_completed_length(0);
group.clear_piece_availability();
group.clear_segment_assignments();
*group.piece_map_mut() = PieceMap::new();
for piece in &metadata.pieces {
group.set_piece_state(PieceId(piece.index), PieceState::Pending);
}
Ok(true)
}
/// Computes the effective total length visible to BitTorrent runtime reporting.
pub(in crate::dispatcher) fn bt_runtime_total_length(group: &RequestGroup) -> u64 {
group.total_length().max(
group
.bt()
.map(BtRuntimeState::selected_or_all_total_length)
.unwrap_or_default(),
)
}
/// Computes the number of pieces required to cover a torrent payload.
pub(in crate::dispatcher) fn bt_piece_count(total_length: u64, piece_length: u64) -> usize {
if total_length == 0 {
0
} else {
total_length.div_ceil(piece_length.max(1)) as usize
}
}
/// Computes the byte span represented by a single BitTorrent piece.
pub(in crate::dispatcher) fn bt_piece_span_bytes(
piece: PieceId,
piece_length: u64,
total_length: u64,
) -> u64 {
let piece_length = piece_length.max(1);
if total_length == 0 {
return piece_length;
}
let start = u64::from(piece.0).saturating_mul(piece_length);
total_length.saturating_sub(start).min(piece_length)
}
/// Sums the verified length implied by the request group's piece map.
pub(in crate::dispatcher) fn bt_verified_length(
group: &RequestGroup,
piece_length: u64,
total_length: u64,
) -> u64 {
group
.piece_map()
.iter()
.filter(|(_, state)| **state == PieceState::Verified)
.map(|(piece, _)| bt_piece_span_bytes(*piece, piece_length, total_length))
.sum()
}
/// Returns whether a BT peer row is usable for an outbound peer-wire exchange.
pub(in crate::dispatcher) fn bt_peer_is_connectable(peer: &BtPeerInfo) -> bool {
!peer.ip.trim().is_empty() && peer.port != 0
}