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