use crate::error::{Result, WebTorrentError};
use url::Url;
#[derive(Debug, Clone)]
pub struct MagnetUri {
pub info_hash: [u8; 20],
pub display_name: Option<String>,
pub trackers: Vec<String>,
pub exact_length: Option<u64>,
pub exact_source: Option<String>,
pub keywords: Vec<String>,
pub acceptable_source: Vec<String>,
}
impl MagnetUri {
pub fn parse(uri: &str) -> Result<Self> {
if !uri.starts_with("magnet:?") {
return Err(WebTorrentError::InvalidTorrent(
"Invalid magnet URI format".to_string(),
));
}
let url = Url::parse(uri)?;
let mut info_hash = None;
let mut display_name = None;
let mut trackers = Vec::new();
let mut exact_length = None;
let mut exact_source = None;
let mut keywords = Vec::new();
let mut acceptable_source = Vec::new();
for (key, value) in url.query_pairs() {
match key.as_ref() {
"xt" => {
if value.starts_with("urn:btih:") {
let hash_str = value.trim_start_matches("urn:btih:");
let hash_bytes = if hash_str.len() == 40 {
hex::decode(hash_str)
.map_err(|e| WebTorrentError::InvalidTorrent(format!("Invalid hex hash: {}", e)))?
} else if hash_str.len() == 32 {
base32::decode(base32::Alphabet::RFC4648 { padding: false }, hash_str)
.ok_or_else(|| WebTorrentError::InvalidTorrent("Invalid base32 hash".to_string()))?
} else {
return Err(WebTorrentError::InvalidTorrent(
"Invalid hash length".to_string(),
));
};
if hash_bytes.len() == 20 {
let mut hash = [0u8; 20];
hash.copy_from_slice(&hash_bytes);
info_hash = Some(hash);
}
}
}
"dn" => {
display_name = Some(value.to_string());
}
"tr" => {
trackers.push(value.to_string());
}
"xl" => {
exact_length = value.parse().ok();
}
"xs" => {
exact_source = Some(value.to_string());
}
"kt" => {
keywords.extend(value.split('+').map(|s| s.to_string()));
}
"as" => {
acceptable_source.push(value.to_string());
}
_ => {}
}
}
let info_hash = info_hash.ok_or_else(|| {
WebTorrentError::InvalidTorrent("Magnet URI missing info hash (xt)".to_string())
})?;
Ok(Self {
info_hash,
display_name,
trackers,
exact_length,
exact_source,
keywords,
acceptable_source,
})
}
pub fn to_string(&self) -> String {
let mut parts = Vec::new();
parts.push(format!("magnet:?xt=urn:btih:{}", hex::encode(self.info_hash)));
if let Some(ref name) = self.display_name {
parts.push(format!("dn={}", urlencoding::encode(name)));
}
for tracker in &self.trackers {
parts.push(format!("tr={}", urlencoding::encode(tracker)));
}
if let Some(length) = self.exact_length {
parts.push(format!("xl={}", length));
}
if let Some(ref source) = self.exact_source {
parts.push(format!("xs={}", urlencoding::encode(source)));
}
if !self.keywords.is_empty() {
parts.push(format!("kt={}", self.keywords.join("+")));
}
for source in &self.acceptable_source {
parts.push(format!("as={}", urlencoding::encode(source)));
}
parts.join("&")
}
}