chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user