chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
//! 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<String>,
|
||||
/// Ordered tracker URLs from `tr` query fields.
|
||||
pub trackers: Vec<String>,
|
||||
/// Ordered web-seed URLs from `ws` query fields.
|
||||
pub web_seeds: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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<MetalinkDocumentModel>,
|
||||
}
|
||||
|
||||
/// 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:<hash>` value.
|
||||
pub fn parse(input: &str) -> Result<Self, BtMetalinkError> {
|
||||
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<Self, BtMetalinkError> {
|
||||
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<Self, TorrentMetadataError> {
|
||||
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#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<metalink version="4.0" xmlns="urn:ietf:params:xml:ns:metalink">
|
||||
<identity>wrapped fixture</identity>
|
||||
<file name="wrapped.iso">
|
||||
<description>real parser payload</description>
|
||||
<url priority="1" location="us">https://mirror.example.com/wrapped.iso</url>
|
||||
</file>
|
||||
</metalink>"#,
|
||||
)
|
||||
.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#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<metalink version="4.0" xmlns="urn:ietf:params:xml:ns:metalink">
|
||||
<identity>wrapper fixture</identity>
|
||||
<file name=" wrapped.iso ">
|
||||
<identity> release-42 </identity>
|
||||
<signature><![CDATA[SIG-WRAP]]></signature>
|
||||
<hash type="SHA256"> AA BB </hash>
|
||||
<url location=" us " maxconnections="8">https://mirror.example.com/wrapped.iso</url>
|
||||
</file>
|
||||
</metalink>"#,
|
||||
)
|
||||
.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#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<metalink version="4.0" xmlns="urn:ietf:params:xml:ns:metalink">
|
||||
<file name="empty.iso">
|
||||
<url priority="1"> </url>
|
||||
</file>
|
||||
</metalink>"#,
|
||||
)
|
||||
.expect_err("root-only stub success should be rejected");
|
||||
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("metalink document contains no resource urls"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user