hightorrent 0.4.0

High-level torrent library which supports Bittorrent v1, v2 and hybrid torrents
Documentation
use fluent_uri::{ParseError as UriParseError, Uri};
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use std::str::FromStr;

/// A source of peers. Can be a [`Tracker`](crate::tracker::Tracker) or a decentralized source.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PeerSource {
    DHT,
    PEX,
    LSD,
    Tracker(Tracker),
}

/// A Bittorrent rendezvous server for peers to find one another.
///
/// This is usually parsed directly from a [`TorrentFile`](crate::torrent_file::TorrentFile)
/// or a [`MagnetLink`](crate::magnet::MagnetLink).
#[derive(Clone, Debug, PartialEq)]
pub struct Tracker {
    /// Tracker URL scheme (usually `ws`, `http(s)`, or `udp`)
    scheme: TrackerScheme,
    /// Complete tracker URL
    url: Uri<String>,
}

impl Tracker {
    /// Generate a new Tracker from a given string URL.
    ///
    /// Will fail if scheme is not "http", "https", "wss" or "udp", unless
    /// the `unknown_tracker_scheme` crate feature is enabled.
    ///
    /// Will also fail if the provided URL is url-encoded.
    pub fn new(url: &str) -> Result<Tracker, TrackerError> {
        let url = Uri::parse(url.to_string())?;
        Tracker::from_url(&url)
    }

    /// Generate a new Tracker from a parsed URL.
    ///
    /// Will fail if scheme is not "http", "https", "wss" or "udp", unless
    /// the `unknown_tracker_scheme` crate feature is enabled.
    pub fn from_url(url: &Uri<String>) -> Result<Tracker, TrackerError> {
        Ok(Tracker {
            scheme: TrackerScheme::from_str(url.scheme().as_str())?,
            url: url.clone(),
        })
    }

    /// Turns a centralized Tracker into a wider PeerSource
    pub fn to_peer_source(&self) -> PeerSource {
        PeerSource::from_tracker(self)
    }

    pub fn scheme(&self) -> &TrackerScheme {
        &self.scheme
    }

    pub fn url(&self) -> &str {
        self.url.as_str()
    }
}

impl<'de> Deserialize<'de> for Tracker {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Tracker::new(&s).map_err(serde::de::Error::custom)
    }
}

impl Serialize for Tracker {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // serializer.serialize(&self.url)
        self.url.serialize(serializer)
    }
}

impl FromStr for Tracker {
    type Err = TrackerError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::new(s)
    }
}

/// A protocol used by a [`Tracker`](crate::tracker::Tracker).
///
/// Does not implement Serialize/Deserialize because it's actually not in the
/// torrent data. It is constructed from the parsed tracker URLs contained in
/// the torrent data.
#[derive(Clone, Debug, PartialEq)]
pub enum TrackerScheme {
    Websocket,
    Http,
    Udp,
    /// An unknown scheme is in the tracker URI.
    ///
    /// This is also the case when there is no scheme, in which
    /// case the domain name may be parsed as a scheme.
    ///
    /// This is disabled by default and required the `unknown_tracker_scheme`
    /// crate feature enabled if you really need to parse broken torrents.
    #[cfg(feature = "unknown_tracker_scheme")]
    Unknown(String),
}

impl FromStr for TrackerScheme {
    type Err = TrackerError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "http" | "https" => Ok(Self::Http),
            "ws" => Ok(Self::Websocket),
            "udp" => Ok(Self::Udp),
            #[cfg(feature = "unknown_tracker_scheme")]
            _ => Ok(Self::Unknown(s.to_string())),
            #[cfg(not(feature = "unknown_tracker_scheme"))]
            _ => Err(TrackerError::InvalidScheme {
                scheme: s.to_string(),
            }),
        }
    }
}

/// Error occurred during parsing a [`Tracker`](crate::tracker::Tracker).
#[derive(Clone, Debug, PartialEq)]
pub enum TrackerError {
    /// Tracker URL could not be parsed because it is malformed.
    ///
    /// I'm not sure under what circumstances this could happen.
    InvalidURL { source: UriParseError },
    /// Tracker scheme is not a known variant.
    ///
    /// This error does not exist when the `unknown_tracker_scheme` crate
    /// feature is enabled.
    InvalidScheme { scheme: String },
}

impl std::fmt::Display for TrackerError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TrackerError::InvalidURL { source } => write!(f, "Invalid URL: {source}"),
            TrackerError::InvalidScheme { scheme } => write!(f, "Invalid scheme: {scheme}"),
        }
    }
}

impl std::error::Error for TrackerError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            TrackerError::InvalidURL { source } => Some(source),
            TrackerError::InvalidScheme { scheme: _ } => None,
        }
    }
}

impl<Input> From<(UriParseError, Input)> for TrackerError {
    fn from(e: (UriParseError, Input)) -> TrackerError {
        TrackerError::InvalidURL { source: e.0 }
    }
}

impl PeerSource {
    /// Generate a new PeerSource from a given string URL.
    ///
    /// Only covers the Tracker variant. Other variants should be
    /// instantiated directly.
    pub fn new(url: &str) -> Result<PeerSource, TrackerError> {
        Ok(Tracker::new(url)?.to_peer_source())
    }

    /// Generate a new PeerSource from a given parsed URL.
    ///
    /// Only covers the Tracker variant. Other variants should be
    /// instantiated directly.
    pub fn from_url(url: &Uri<String>) -> Result<PeerSource, TrackerError> {
        Ok(Tracker::from_url(url)?.to_peer_source())
    }

    pub fn from_tracker(tracker: &Tracker) -> PeerSource {
        PeerSource::Tracker(tracker.clone())
    }
}

/// Turn a backend-specific tracker struct into an agnostic [`Tracker`](crate::tracker::Tracker).
pub trait TryIntoTracker {
    fn try_into_tracker(&self) -> Result<Tracker, TrackerError>;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[cfg(not(feature = "unknown_tracker_scheme"))]
    fn fail_unknown_tracker_scheme() {
        let tracker = Tracker::new("wtf://example.com:8000/announce");
        assert!(tracker.is_err());
        assert_eq!(
            tracker.unwrap_err(),
            TrackerError::InvalidScheme {
                scheme: "wtf".to_string()
            },
        );
    }

    #[test]
    fn fail_urlencoded_tracker() {
        let tracker = Tracker::new("http%3F%2A%2A127.0.0.1:8000%2Aannounce");
        assert!(tracker.is_err());
    }

    #[test]
    fn parse_ipv4_literal() {
        let tracker = Tracker::new("http://127.0.0.1:8000/announce");
        assert!(tracker.is_ok());
    }

    #[test]
    fn parse_ipv6_literal() {
        let tracker = Tracker::new("http://[::1]:8000/announce");
        assert!(tracker.is_ok());
    }
}