diff --git a/CHANGELOG.md b/CHANGELOG.md index c30c15b..81840b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 other variants, and other rust types - `TrackerScheme` no longer derives de/serialize because that's not actually used in torrent files +- A torrent file with an invalid tracker URI (such as an unknown scheme) will now + fail to parse as a `DecodedTorrent` and therefore as a `TorrentFile`, unless the + `unknown_tracker_scheme` variant is enabled, in which case it will produce a + valid `TorrentFile` where the `TrackerScheme` is of the `Unknown` variant ### Added @@ -31,6 +35,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 a parsed magnet link. - `MagnetLink::trackers` lists the trackers in the magnet link - `TrackerScheme` and `Tracker` implement `FromStr` +- `TorrentFile::to_vec` serializes to a bencoded byte slice, to save to + a .torrent file +- `DecodedTorrent::announce` and `DecodedTorrent::announce_list` list the + trackers contained in the torrent file +- `TrackerScheme::Unknown` stores unknown schemes instead of failing to parse, + when the tracker URL scheme is not recognized, and when the `unknown_tracker_scheme` + crate feature is anbled ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 024a324..2f85f80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,9 +32,16 @@ serde_json = "1" [features] magnet_force_name = [] +unknown_tracker_scheme = [] [[test]] name = "magnet_force_name" path = "tests/magnet_force_name.rs" required-features = [ "magnet_force_name" ] test = true + +[[test]] +name = "unknown_tracker_scheme" +path = "tests/unknown_tracker_scheme.rs" +required-features = [ "unknown_tracker_scheme" ] +test = true diff --git a/src/torrent_file.rs b/src/torrent_file.rs index e052db4..b3d0b93 100644 --- a/src/torrent_file.rs +++ b/src/torrent_file.rs @@ -6,7 +6,7 @@ use sha1::{Digest, Sha1}; use std::collections::HashMap; use std::path::PathBuf; -use crate::{InfoHash, InfoHashError, PieceLength, TorrentContent, TorrentID}; +use crate::{InfoHash, InfoHashError, PieceLength, TorrentContent, TorrentID, Tracker}; /// Error occurred during parsing a [`TorrentFile`](crate::torrent_file::TorrentFile). #[derive(Clone, Debug, PartialEq)] @@ -89,11 +89,24 @@ pub struct TorrentFile { /// A parsed bencode-decoded value, to ensure torrent-like structure. /// -/// In its present form, DecodedTorrent only cares about the info dict, but preserves other fields +/// In its present form, DecodedTorrent mostly cares about the info dict, but preserves other fields /// as [`BencodeValue`](bt_bencode::BencodeValue) in an `extra` mapping so you can implement /// your own extra parsing. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DecodedTorrent { + /// Main tracker + #[serde(skip_serializing_if = "Option::is_none")] + announce: Option, + + /// Many alternative trackers. + /// TODO: what is this about the tiers? + #[serde( + rename = "announce-list", + default, + skip_serializing_if = "Vec::is_empty" + )] + announce_list: Vec>, + info: DecodedInfo, // Rest of torrent dict @@ -234,6 +247,13 @@ pub struct DecodedInfo { } impl TorrentFile { + /// Serialize to a .torrent file byte slice + pub fn to_vec(&self) -> Vec { + // This should not fail + bt_bencode::to_vec(&self.decoded).unwrap() + } + + /// Deserialize (parse) from a .torrent file byte slice pub fn from_slice(s: &[u8]) -> Result { let torrent: DecodedTorrent = bt_bencode::from_slice(s).map_err(|e| { // We store a stringy representation of the error because bt_encode::Error @@ -309,7 +329,7 @@ mod tests { use super::*; #[test] - fn can_read_torrent_v1() { + fn can_read_torrent_v1_multifile() { let slice = std::fs::read("tests/bittorrent-v1-emma-goldman.torrent").unwrap(); let res = TorrentFile::from_slice(&slice); println!("{:?}", res); @@ -327,29 +347,17 @@ mod tests { } #[test] - fn can_read_torrent_v1_multifile() { + #[cfg(not(feature = "unknown_tracker_scheme"))] + fn fail_no_torrent_scheme() { let slice = std::fs::read("tests/libtorrent/good/sample.torrent").unwrap(); let res = TorrentFile::from_slice(&slice); println!("{:?}", res); - assert!(res.is_ok()); - let torrent = res.unwrap(); - assert_eq!(&torrent.name, "sample"); - assert_eq!( - torrent.hash, - InfoHash::V1("58d8d15a4eb3bd9afabc9cee2564f78192777edb".to_string()) - ); + assert!(res.is_err()); assert_eq!( - torrent.decoded.files().unwrap(), - vec!( - TorrentContent { - path: PathBuf::from("text_file.txt"), - size: 20, - }, - TorrentContent { - path: PathBuf::from("text_file2.txt"), - size: 25, - } - ), + res.unwrap_err(), + TorrentFileError::NotATorrent { + reason: "Invalid scheme: tracker.publicbt.com".to_string() + }, ); } diff --git a/src/tracker.rs b/src/tracker.rs index 2838420..6896c0a 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -88,6 +88,15 @@ 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 { @@ -98,6 +107,9 @@ impl FromStr for TrackerScheme { "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(), }), diff --git a/tests/unknown_tracker_scheme.rs b/tests/unknown_tracker_scheme.rs new file mode 100644 index 0000000..2b8c9d2 --- /dev/null +++ b/tests/unknown_tracker_scheme.rs @@ -0,0 +1,31 @@ +use hightorrent::{InfoHash, TorrentContent, TorrentFile}; + +use std::path::PathBuf; + +#[test] +fn can_parse_no_scheme_tracker() { + // This only works when the unknown_tracker_scheme crate feature is eanbled + let slice = std::fs::read("tests/libtorrent/good/sample.torrent").unwrap(); + let res = TorrentFile::from_slice(&slice); + println!("{:?}", res); + assert!(res.is_ok()); + let torrent = res.unwrap(); + assert_eq!(&torrent.name, "sample"); + assert_eq!( + torrent.hash, + InfoHash::V1("58d8d15a4eb3bd9afabc9cee2564f78192777edb".to_string()) + ); + assert_eq!( + torrent.decoded.files().unwrap(), + vec!( + TorrentContent { + path: PathBuf::from("text_file.txt"), + size: 20, + }, + TorrentContent { + path: PathBuf::from("text_file2.txt"), + size: 25, + } + ), + ); +}