//! Compatibility wrappers that bridge BitTorrent, magnet, and Metalink models. #![forbid(unsafe_code)] use std::fmt::{Display, Formatter}; use crate::{ magnet::{MagnetUriModel, parse_magnet_uri}, metalink::{MetalinkDocumentModel, parse_metalink_document}, torrent::{TorrentMetadataModel, parse_torrent_metadata}, }; /// Declares how far a protocol family has progressed in the current implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProtocolSupportState { /// The protocol is known but not yet wired into the workspace. Planned, /// Parsing and model registration exist, but transfer execution is pending. Registered, /// A compatibility skeleton is present and exposes the public API shape. Skeleton, } /// Summarizes protocol readiness across BitTorrent-adjacent inputs. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ProtocolSupportMatrix { /// Current state of `.torrent` metadata handling. pub bit_torrent: ProtocolSupportState, /// Current state of magnet URI handling. pub magnet: ProtocolSupportState, /// Current state of Metalink XML handling. pub metalink: ProtocolSupportState, } /// Returns the current protocol support matrix exposed by this compatibility layer. #[must_use] pub const fn protocol_support_matrix() -> ProtocolSupportMatrix { ProtocolSupportMatrix { bit_torrent: ProtocolSupportState::Skeleton, magnet: ProtocolSupportState::Registered, metalink: ProtocolSupportState::Registered, } } /// Compatibility wrapper for parsed magnet URI metadata. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MagnetUri { /// Parsed BTIH info hash. pub info_hash: String, /// Optional display name from the `dn` query field. pub display_name: Option, /// Ordered tracker URLs from `tr` query fields. pub trackers: Vec, /// Ordered web-seed URLs from `ws` query fields. pub web_seeds: Vec, } /// Compatibility wrapper for parsed Metalink documents. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MetalinkDocument { /// Root element name used for diagnostics. pub root_element: String, /// Parser state describing whether a real model was produced. pub status: ParserStatus, /// Parsed Metalink document model when parsing succeeded. pub document: Option, } /// Records whether a compatibility parser stayed stubbed or produced a model. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ParserStatus { /// Parsing support is registered but no real model was built. RegisteredStub, /// Parsing produced a protocol-layer document model. ParsedModel, } /// Errors raised while converting BitTorrent-adjacent formats into compatibility models. #[derive(Debug, Clone, PartialEq, Eq)] pub enum BtMetalinkError { /// The magnet URI was malformed. InvalidMagnet { /// Parser-specific explanation of the magnet failure. reason: String, }, /// The requested torrent feature is not yet implemented. UnsupportedTorrentBinary, /// The Metalink document was malformed or unsupported. InvalidMetalink { /// Parser-specific explanation of the Metalink failure. reason: String, }, } impl Display for BtMetalinkError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::InvalidMagnet { reason } => write!(f, "invalid magnet: {reason}"), Self::UnsupportedTorrentBinary => { f.write_str("torrent binary parsing is not implemented") } Self::InvalidMetalink { reason } => write!(f, "invalid metalink: {reason}"), } } } impl std::error::Error for BtMetalinkError {} impl MagnetUri { /// Builds the compatibility wrapper from the protocol-layer model. #[must_use] pub fn from_model(model: MagnetUriModel) -> Self { Self { info_hash: model.info_hash, display_name: model.display_name, trackers: model.trackers, web_seeds: model.web_seeds, } } /// Parses a magnet URI into the compatibility skeleton. /// /// # Errors /// /// Returns an error when the `magnet:?` prefix is missing or the URI does /// not contain an `xt=urn:btih:` value. pub fn parse(input: &str) -> Result { parse_magnet_uri(input).map(Self::from_model) } } impl MetalinkDocument { /// Builds the compatibility wrapper from the protocol-layer Metalink model. #[must_use] pub fn from_model(model: MetalinkDocumentModel) -> Self { Self { root_element: "metalink".to_owned(), status: ParserStatus::ParsedModel, document: Some(model), } } /// Parses Metalink XML text through the protocol-layer Metalink parser. /// /// # Errors /// /// Returns an error when the input is not a valid Metalink document or /// when the document has no actionable resources. pub fn parse(input: &str) -> Result { let document = parse_metalink_document(input) .map_err(|reason| BtMetalinkError::InvalidMetalink { reason })?; Ok(Self { root_element: "metalink".to_owned(), status: ParserStatus::ParsedModel, document: Some(document), }) } } /// Compatibility wrapper for parsed torrent metadata. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TorrentMetadata { /// Parsed torrent metadata model. pub model: TorrentMetadataModel, } /// Errors raised while parsing `.torrent` metadata. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TorrentMetadataError { /// The torrent payload was syntactically invalid. Invalid { /// Parser-specific explanation of the torrent failure. reason: String, }, /// The payload used an unsupported feature. Unsupported { /// Name of the unsupported torrent feature. feature: &'static str, }, } impl Display for TorrentMetadataError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Invalid { reason } => write!(f, "invalid torrent metadata: {reason}"), Self::Unsupported { feature } => write!(f, "{feature} is not implemented"), } } } impl std::error::Error for TorrentMetadataError {} impl TorrentMetadata { /// Wraps a protocol-layer torrent model. #[must_use] pub fn from_model(model: TorrentMetadataModel) -> Self { Self { model } } /// Returns a shared reference to the underlying torrent metadata model. #[must_use] pub fn as_model(&self) -> &TorrentMetadataModel { &self.model } /// Consumes the wrapper and returns the underlying torrent metadata model. #[must_use] pub fn into_model(self) -> TorrentMetadataModel { self.model } /// Parses torrent binary metadata through the shared protocol-layer parser. /// /// # Errors /// /// Returns a structured invalid-metadata error when the payload cannot be parsed. pub fn parse(input: &[u8]) -> Result { parse_torrent_metadata(input) .map(Self::from_model) .map_err(|error| TorrentMetadataError::Invalid { reason: error }) } } #[cfg(test)] mod tests { use super::{ MagnetUri, MetalinkDocument, ParserStatus, TorrentMetadata, TorrentMetadataError, parse_magnet_uri, }; use crate::{ TorrentFileEntryModel, TorrentInfoModel, TorrentMetadataModel, TorrentPeerModel, TorrentTrackerModel, }; #[test] fn magnet_wrapper_parse_matches_magnet_model_fields() { let input = "magnet:?xt=urn:btih:0123456789abcdef&dn=Ubuntu%2024.04&tr=http%3A%2F%2Ftracker.example.org%2Fannounce&ws=https%3A%2F%2Fcdn.example.org%2Fubuntu.iso"; let parsed = MagnetUri::parse(input).expect("magnet wrapper should parse"); let model = parse_magnet_uri(input).expect("protocol magnet parser should parse"); assert_eq!(parsed.info_hash, model.info_hash); assert_eq!(parsed.display_name, model.display_name); assert_eq!(parsed.trackers, model.trackers); assert_eq!(parsed.web_seeds, model.web_seeds); } #[test] fn torrent_wrapper_round_trips_model_without_changing_shape() { let model = TorrentMetadataModel { info: TorrentInfoModel { name: "sample".to_owned(), piece_length: 16, pieces: vec![[0_u8; 20]], files: vec![TorrentFileEntryModel { path: "sample.bin".to_owned(), length: 16, piece_offset: Some(0), selected: true, }], hash: None, private: false, }, announce: Some("http://tracker.example.org/announce".to_owned()), trackers: vec![TorrentTrackerModel { url: "http://tracker.example.org/announce".to_owned(), tier: Some(1), id: None, seeders: None, leechers: None, }], peers: vec![TorrentPeerModel { peer_id: None, ip: "127.0.0.1".to_owned(), port: 6881, client_name: None, interested: false, choked: true, }], dht_nodes: vec!["router.example.org:6881".to_owned()], pieces: Vec::new(), creation_date: None, comment: None, }; let wrapper = TorrentMetadata::from_model(model.clone()); assert_eq!(wrapper.as_model(), &model); assert_eq!(wrapper.into_model(), model); } #[test] fn torrent_wrapper_surfaces_parse_errors_as_invalid_metadata() { let error = TorrentMetadata::parse(b"not-a-torrent").expect_err("bad torrent should fail"); match error { TorrentMetadataError::Invalid { reason } => assert!(!reason.is_empty()), TorrentMetadataError::Unsupported { feature } => { panic!("unexpected torrent error variant: unsupported feature {feature}") } } } #[test] fn metalink_wrapper_parse_returns_real_parsed_model() { let parsed = MetalinkDocument::parse( r#" wrapped fixture real parser payload https://mirror.example.com/wrapped.iso "#, ) .expect("metalink wrapper should parse through the real model"); assert_eq!(parsed.root_element, "metalink"); assert_eq!(parsed.status, ParserStatus::ParsedModel); let document = parsed .document .expect("wrapper should retain parsed document"); assert_eq!(document.version.as_deref(), Some("4.0")); assert_eq!(document.identity.as_deref(), Some("wrapped fixture")); assert_eq!(document.files.len(), 1); assert_eq!(document.files[0].name, "wrapped.iso"); assert_eq!( document.files[0].description.as_deref(), Some("real parser payload") ); assert_eq!( document.files[0].resources[0].url, "https://mirror.example.com/wrapped.iso" ); } #[test] fn metalink_wrapper_preserves_normalized_file_metadata_and_resource_hints() { let parsed = MetalinkDocument::parse( r#" wrapper fixture release-42 AA BB https://mirror.example.com/wrapped.iso "#, ) .expect("metalink wrapper should preserve normalized parser output"); let document = parsed .document .expect("wrapper should retain parsed document"); assert_eq!(document.identity.as_deref(), Some("wrapper fixture")); assert_eq!(document.files[0].name, "wrapped.iso"); assert_eq!(document.files[0].identifier.as_deref(), Some("release-42")); assert_eq!(document.files[0].signatures, vec!["SIG-WRAP".to_owned()]); assert_eq!(document.files[0].checksums[0].algorithm, "sha-256"); assert_eq!(document.files[0].checksums[0].value, "aabb"); assert_eq!( document.files[0].resources[0].location.as_deref(), Some("us") ); assert_eq!(document.files[0].resources[0].max_connections, Some(8)); assert_eq!( document.files[0].resources[0].type_hint.as_deref(), Some("https") ); } #[test] fn metalink_wrapper_parse_rejects_invalid_root_only_stub_shape() { let error = MetalinkDocument::parse( r#" "#, ) .expect_err("root-only stub success should be rejected"); assert!( error .to_string() .contains("metalink document contains no resource urls"), "unexpected error: {error}" ); } }