Files
aria2-rust-pro/crates/aria2-rust-pro-protocol/src/torrent/metadata.rs
T

186 lines
6.4 KiB
Rust

use sha1::{Digest, Sha1};
use super::{
bencode::{BencodeValue, TorrentBencodeDict, dict_bytes, dict_int, parse_root_dict},
model::{
TorrentBootstrapModel, TorrentFileEntryModel, TorrentHashModel, TorrentInfoModel,
TorrentMetadataModel, TorrentPieceModel, TorrentTrackerModel,
},
utils::{bytes_to_string, hex_encode, i64_to_u64, value_to_string},
};
/// Parses a `.torrent` payload into structured metadata.
///
/// # Errors
///
/// Returns an error when the bencoded payload is malformed or lacks the required info dictionary.
pub fn parse_torrent_metadata(input: &[u8]) -> Result<TorrentMetadataModel, String> {
let (root, info_raw) = parse_root_dict(input)?;
let info_map = match root.get("info") {
Some(BencodeValue::Dict(map)) => map,
Some(_) => return Err("torrent info must be a dictionary".to_owned()),
None => return Err("missing torrent info dictionary".to_owned()),
};
let info_hash_hex = hex_encode(&Sha1::digest(
info_raw.ok_or_else(|| "missing info bytes".to_owned())?,
));
let info = parse_info_model(info_map, info_hash_hex);
let announce = dict_bytes(&root, "announce").map(bytes_to_string);
let creation_date = dict_bytes(&root, "creation date").map(bytes_to_string);
let comment = dict_bytes(&root, "comment").map(bytes_to_string);
Ok(TorrentMetadataModel {
pieces: build_piece_models(&info),
info,
announce,
trackers: parse_trackers(&root),
peers: Vec::new(),
dht_nodes: parse_dht_nodes(&root),
creation_date,
comment,
})
}
/// Parses a `.torrent` payload and immediately shapes it into bootstrap-ready metadata.
///
/// # Errors
///
/// Returns an error when the `.torrent` payload is malformed or the derived bootstrap fields
/// cannot be shaped.
pub fn parse_torrent_bootstrap(input: &[u8]) -> Result<TorrentBootstrapModel, String> {
parse_torrent_metadata(input)?.bootstrap()
}
/// Builds the higher-level torrent info model from the parsed `info` dictionary.
fn parse_info_model(info: &TorrentBencodeDict, info_hash_hex: String) -> TorrentInfoModel {
let name = dict_bytes(info, "name")
.map(bytes_to_string)
.unwrap_or_default();
let piece_length = i64_to_u64(dict_int(info, "piece length").unwrap_or_default());
let pieces = dict_bytes(info, "pieces")
.map(|bytes| {
bytes
.chunks_exact(20)
.map(|chunk| {
let mut hash = [0_u8; 20];
hash.copy_from_slice(chunk);
hash
})
.collect()
})
.unwrap_or_default();
let private = dict_int(info, "private").is_some_and(|value| value != 0);
let hash = Some(TorrentHashModel {
info_hash_hex,
info_hash_base32: None,
});
let files = if let Some(BencodeValue::List(entries)) = info.get("files") {
let mut offset = 0_u64;
entries
.iter()
.filter_map(|entry| match entry {
BencodeValue::Dict(file) => {
let length = i64_to_u64(dict_int(file, "length").unwrap_or_default());
let path = match file.get("path") {
Some(BencodeValue::List(parts)) => parts
.iter()
.map(value_to_string)
.collect::<Vec<_>>()
.join("/"),
_ => String::new(),
};
let item = TorrentFileEntryModel {
path,
length,
piece_offset: Some(offset),
selected: true,
};
offset = offset.saturating_add(length);
Some(item)
}
_ => None,
})
.collect()
} else {
vec![TorrentFileEntryModel {
path: name.clone(),
length: i64_to_u64(dict_int(info, "length").unwrap_or_default()),
piece_offset: Some(0),
selected: true,
}]
};
TorrentInfoModel {
name,
piece_length,
pieces,
files,
hash,
private,
}
}
/// Derives piece descriptors with offsets and effective lengths from torrent metadata.
fn build_piece_models(info: &TorrentInfoModel) -> Vec<TorrentPieceModel> {
info.pieces
.iter()
.enumerate()
.filter_map(|(index, hash)| {
u32::try_from(index).ok().map(|index| TorrentPieceModel {
index,
hash: *hash,
length: info.piece_length,
})
})
.collect()
}
/// Parses the primary announce URL plus announce-list tiers into stable tracker entries.
fn parse_trackers(root: &TorrentBencodeDict) -> Vec<TorrentTrackerModel> {
let mut trackers = Vec::new();
if let Some(url) = dict_bytes(root, "announce").map(bytes_to_string) {
trackers.push(TorrentTrackerModel {
url,
tier: Some(0),
id: None,
seeders: None,
leechers: None,
});
}
if let Some(BencodeValue::List(tiers)) = root.get("announce-list") {
for (tier_index, tier) in tiers.iter().enumerate() {
if let BencodeValue::List(urls) = tier {
let tier = u32::try_from(tier_index)
.ok()
.and_then(|value| value.checked_add(1));
for url in urls {
trackers.push(TorrentTrackerModel {
url: value_to_string(url),
tier,
id: None,
seeders: None,
leechers: None,
});
}
}
}
}
trackers
}
/// Parses DHT bootstrap nodes from the optional `nodes` field.
fn parse_dht_nodes(root: &TorrentBencodeDict) -> Vec<String> {
match root.get("nodes") {
Some(BencodeValue::List(nodes)) => nodes
.iter()
.filter_map(|node| match node {
BencodeValue::List(parts) => parts.first().zip(parts.get(1)).map(|(host, port)| {
format!("{}:{}", value_to_string(host), value_to_string(port))
}),
_ => None,
})
.collect(),
_ => Vec::new(),
}
}