From 0836b651a7a4e056b5a60a7baa35e534cfe96042 Mon Sep 17 00:00:00 2001 From: Joshua Abraham Date: Sun, 24 Dec 2023 16:53:02 -0600 Subject: [PATCH] Add support for BTv2 magnet links --- crates/dht/examples/dht.rs | 2 +- crates/dht/src/bprotocol.rs | 4 +- crates/dht/src/dht.rs | 2 +- crates/dht/src/lib.rs | 2 +- crates/dht/src/peer_store.rs | 2 +- crates/dht/src/routing_table.rs | 20 +- crates/dht/src/utils.rs | 2 +- crates/librqbit/src/dht_utils.rs | 2 +- crates/librqbit/src/peer_connection.rs | 6 +- crates/librqbit/src/peer_info_reader/mod.rs | 4 +- crates/librqbit/src/session.rs | 10 +- crates/librqbit/src/torrent_state/live/mod.rs | 8 +- .../src/torrent_state/live/peer/mod.rs | 2 +- crates/librqbit/src/torrent_state/mod.rs | 2 +- crates/librqbit/src/tracker_comms.rs | 6 +- .../librqbit_core/src/{id20.rs => hash_id.rs} | 179 ++++++++++-------- crates/librqbit_core/src/lib.rs | 2 +- crates/librqbit_core/src/magnet.rs | 86 +++++++-- crates/librqbit_core/src/peer_id.rs | 4 +- crates/librqbit_core/src/torrent_metainfo.rs | 4 +- crates/peer_binary_protocol/src/lib.rs | 6 +- 21 files changed, 217 insertions(+), 138 deletions(-) rename crates/librqbit_core/src/{id20.rs => hash_id.rs} (59%) diff --git a/crates/dht/examples/dht.rs b/crates/dht/examples/dht.rs index ec436ad3..586a7077 100644 --- a/crates/dht/examples/dht.rs +++ b/crates/dht/examples/dht.rs @@ -12,7 +12,7 @@ async fn main() -> anyhow::Result<()> { .nth(1) .expect("first argument should be a magnet link"); let magnet = Magnet::parse(&magnet).unwrap(); - let info_hash = magnet.info_hash; + let info_hash = magnet.as_id20().context("Supplied magnet link didn't contain a BTv1 infohash")?; tracing_subscriber::fmt::init(); diff --git a/crates/dht/src/bprotocol.rs b/crates/dht/src/bprotocol.rs index 4e2e8ebd..2cbe1704 100644 --- a/crates/dht/src/bprotocol.rs +++ b/crates/dht/src/bprotocol.rs @@ -6,7 +6,7 @@ use std::{ use bencode::{ByteBuf, ByteString}; use clone_to_owned::CloneToOwned; -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; use serde::{ de::{IgnoredAny, Unexpected}, Deserialize, Deserializer, Serialize, @@ -229,7 +229,7 @@ impl<'de> Deserialize<'de> for CompactNodeInfo { let ip = Ipv4Addr::new(chunk[20], chunk[21], chunk[22], chunk[23]); let port = ((chunk[24] as u16) << 8) + chunk[25] as u16; buf.push(Node { - id: Id20(node_id), + id: Id20::new(node_id), addr: SocketAddrV4::new(ip, port), }) } diff --git a/crates/dht/src/dht.rs b/crates/dht/src/dht.rs index e97762ec..d983f9c5 100644 --- a/crates/dht/src/dht.rs +++ b/crates/dht/src/dht.rs @@ -27,7 +27,7 @@ use futures::{stream::FuturesUnordered, Stream, StreamExt, TryFutureExt}; use leaky_bucket::RateLimiter; use librqbit_core::{ - id20::Id20, + hash_id::Id20, peer_id::generate_peer_id, spawn_utils::{spawn, spawn_with_cancel}, }; diff --git a/crates/dht/src/lib.rs b/crates/dht/src/lib.rs index 94188d02..7096d520 100644 --- a/crates/dht/src/lib.rs +++ b/crates/dht/src/lib.rs @@ -10,7 +10,7 @@ use std::time::Duration; pub use crate::dht::DhtStats; pub use crate::dht::{DhtConfig, DhtState, RequestPeersStream}; -pub use librqbit_core::id20::Id20; +pub use librqbit_core::hash_id::Id20; pub use persistence::{PersistentDht, PersistentDhtConfig}; pub type Dht = Arc; diff --git a/crates/dht/src/peer_store.rs b/crates/dht/src/peer_store.rs index 410b5b42..2a20e3ff 100644 --- a/crates/dht/src/peer_store.rs +++ b/crates/dht/src/peer_store.rs @@ -7,7 +7,7 @@ use std::{ use bencode::ByteString; use chrono::{DateTime, Utc}; -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; use parking_lot::RwLock; use rand::RngCore; use serde::{ diff --git a/crates/dht/src/routing_table.rs b/crates/dht/src/routing_table.rs index 1fe85bcc..8f4e7e88 100644 --- a/crates/dht/src/routing_table.rs +++ b/crates/dht/src/routing_table.rs @@ -1,6 +1,6 @@ use std::{net::SocketAddr, time::Instant}; -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; use rand::RngCore; use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; use tracing::{debug, trace}; @@ -132,7 +132,7 @@ impl<'a> Iterator for BucketTreeIterator<'a> { pub fn generate_random_id(start: &Id20, bits: u8) -> Id20 { let mut data = [0u8; 20]; rand::thread_rng().fill_bytes(&mut data); - let mut data = Id20(data); + let mut data = Id20::new(data); let remaining_bits = 160 - bits; for bit in 0..remaining_bits { data.set_bit(bit, start.get_bit(bit)); @@ -199,8 +199,8 @@ impl BucketTree { BucketTree { data: vec![BucketTreeNode { bits: 160, - start: Id20([0u8; 20]), - end_inclusive: Id20([0xff; 20]), + start: Id20::new([0u8; 20]), + end_inclusive: Id20::new([0xff; 20]), data: BucketTreeNodeData::Leaf(Default::default()), }], size: 0, @@ -583,7 +583,7 @@ mod tests { str::FromStr, }; - use librqbit_core::id20::Id20; + use librqbit_core::hash_id::Id20; use rand::Rng; use crate::routing_table::compute_split_start_end; @@ -592,8 +592,8 @@ mod tests { #[test] fn compute_split_start_end_root() { - let start = Id20([0u8; 20]); - let end = Id20([0xff; 20]); + let start = Id20::new([0u8; 20]); + let end = Id20::new([0xff; 20]); assert_eq!( compute_split_start_end(start, end, 160), ( @@ -612,7 +612,7 @@ mod tests { #[test] fn compute_split_start_end_second_split() { let start = Id20::from_str("8000000000000000000000000000000000000000").unwrap(); - let end = Id20([0xff; 20]); + let end = Id20::new([0xff; 20]); assert_eq!( compute_split_start_end(start, end, 159), ( @@ -631,7 +631,7 @@ mod tests { #[test] fn compute_split_start_end_3() { let start = Id20::from_str("8000000000000000000000000000000000000000").unwrap(); - let end = Id20([0xff; 20]); + let end = Id20::new([0xff; 20]); assert_eq!( compute_split_start_end(start, end, 159), ( @@ -650,7 +650,7 @@ mod tests { fn random_id_20() -> Id20 { let mut id20 = [0u8; 20]; rand::thread_rng().fill(&mut id20); - Id20(id20) + Id20::new(id20) } fn generate_socket_addr() -> SocketAddr { diff --git a/crates/dht/src/utils.rs b/crates/dht/src/utils.rs index 59d72368..b38e8ce2 100644 --- a/crates/dht/src/utils.rs +++ b/crates/dht/src/utils.rs @@ -1,4 +1,4 @@ -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; use serde::Serializer; pub fn serialize_id20(id: &Id20, ser: S) -> Result diff --git a/crates/librqbit/src/dht_utils.rs b/crates/librqbit/src/dht_utils.rs index d3357ec8..747d1bda 100644 --- a/crates/librqbit/src/dht_utils.rs +++ b/crates/librqbit/src/dht_utils.rs @@ -9,7 +9,7 @@ use tracing::debug; use crate::{ peer_connection::PeerConnectionOptions, peer_info_reader, spawn_utils::BlockingSpawner, }; -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; #[derive(Debug)] pub enum ReadMetainfoResult { diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index 9a429bf4..3ce8d544 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::{bail, Context}; use buffers::{ByteBuf, ByteString}; use clone_to_owned::CloneToOwned; -use librqbit_core::{id20::Id20, lengths::ChunkInfo, peer_id::try_decode_peer_id}; +use librqbit_core::{hash_id::Id20, lengths::ChunkInfo, peer_id::try_decode_peer_id}; use parking_lot::RwLock; use peer_binary_protocol::{ extended::{handshake::ExtendedHandshake, ExtendedMessage}, @@ -149,7 +149,7 @@ impl PeerConnection { trace!( "incoming connection: id={:?}", - try_decode_peer_id(Id20(handshake.peer_id)) + try_decode_peer_id(Id20::new(handshake.peer_id)) ); let mut write_buf = Vec::::with_capacity(PIECE_MESSAGE_DEFAULT_LEN); @@ -217,7 +217,7 @@ impl PeerConnection { .map_err(|e| anyhow::anyhow!("error deserializing handshake: {:?}", e))?; let h_supports_extended = h.supports_extended(); - trace!("connected: id={:?}", try_decode_peer_id(Id20(h.peer_id))); + trace!("connected: id={:?}", try_decode_peer_id(Id20::new(h.peer_id))); if h.info_hash != self.info_hash.0 { anyhow::bail!("info hash does not match"); } diff --git a/crates/librqbit/src/peer_info_reader/mod.rs b/crates/librqbit/src/peer_info_reader/mod.rs index f0a52a55..9cea3861 100644 --- a/crates/librqbit/src/peer_info_reader/mod.rs +++ b/crates/librqbit/src/peer_info_reader/mod.rs @@ -4,7 +4,7 @@ use bencode::from_bytes; use buffers::{ByteBuf, ByteString}; use librqbit_core::{ constants::CHUNK_SIZE, - id20::Id20, + hash_id::Id20, lengths::{ceil_div_u64, last_element_size_u64, ChunkInfo}, torrent_metainfo::TorrentMetaV1Info, }; @@ -226,7 +226,7 @@ impl PeerConnectionHandler for Handler { mod tests { use std::{net::SocketAddr, str::FromStr, sync::Once}; - use librqbit_core::id20::Id20; + use librqbit_core::hash_id::Id20; use librqbit_core::peer_id::generate_peer_id; use crate::spawn_utils::BlockingSpawner; diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index c825988f..d0e385df 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -552,7 +552,7 @@ impl Session { )); } - bail!("didn't find a matching torrent for {:?}", Id20(h.info_hash)) + bail!("didn't find a matching torrent for {:?}", Id20::new(h.info_hash)) } async fn task_tcp_listener(self: Arc, l: TcpListener) -> anyhow::Result<()> { @@ -757,10 +757,8 @@ impl Session { let (info_hash, info, dht_rx, trackers, initial_peers) = match add { AddTorrent::Url(magnet) if magnet.starts_with("magnet:") => { - let Magnet { - info_hash, - trackers, - } = Magnet::parse(&magnet).context("provided path is not a valid magnet URL")?; + let magnet = Magnet::parse(&magnet).context("provided path is not a valid magnet URL")?; + let info_hash = magnet.as_id20().context("magnet link didn't contain a BTv1 infohash")?; let dht_rx = self .dht @@ -768,7 +766,7 @@ impl Session { .context("magnet links without DHT are not supported")? .get_peers(info_hash, announce_port)?; - let trackers = trackers + let trackers = magnet.trackers .into_iter() .filter_map(|url| match reqwest::Url::parse(&url) { Ok(url) => Some(url), diff --git a/crates/librqbit/src/torrent_state/live/mod.rs b/crates/librqbit/src/torrent_state/live/mod.rs index c439df2c..0724993a 100644 --- a/crates/librqbit/src/torrent_state/live/mod.rs +++ b/crates/librqbit/src/torrent_state/live/mod.rs @@ -63,7 +63,7 @@ use clone_to_owned::CloneToOwned; use futures::{stream::FuturesUnordered, StreamExt}; use itertools::Itertools; use librqbit_core::{ - id20::Id20, + hash_id::Id20, lengths::{ChunkInfo, Lengths, ValidPieceIndex}, spawn_utils::spawn_with_cancel, speed_estimator::SpeedEstimator, @@ -383,7 +383,7 @@ impl TorrentStateLive { let peer = occ.get_mut(); peer.state .incoming_connection( - Id20(checked_peer.handshake.peer_id), + Id20::new(checked_peer.handshake.peer_id), tx.clone(), &self.peers.stats, ) @@ -393,7 +393,7 @@ impl TorrentStateLive { Entry::Vacant(vac) => { atomic_inc(&self.peers.stats.seen); let peer = Peer::new_live_for_incoming_connection( - Id20(checked_peer.handshake.peer_id), + Id20::new(checked_peer.handshake.peer_id), tx.clone(), &self.peers.stats, ); @@ -598,7 +598,7 @@ impl TorrentStateLive { fn set_peer_live(&self, handle: PeerHandle, h: Handshake) { self.peers.with_peer_mut(handle, "set_peer_live", |p| { p.state - .connecting_to_live(Id20(h.peer_id), &self.peers.stats); + .connecting_to_live(Id20::new(h.peer_id), &self.peers.stats); }); } diff --git a/crates/librqbit/src/torrent_state/live/peer/mod.rs b/crates/librqbit/src/torrent_state/live/peer/mod.rs index a915999b..014c975c 100644 --- a/crates/librqbit/src/torrent_state/live/peer/mod.rs +++ b/crates/librqbit/src/torrent_state/live/peer/mod.rs @@ -2,7 +2,7 @@ pub mod stats; use std::collections::HashSet; -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; use librqbit_core::lengths::{ChunkInfo, ValidPieceIndex}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index 92f1c700..7d29b794 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -16,7 +16,7 @@ use anyhow::bail; use anyhow::Context; use buffers::ByteString; use dht::RequestPeersStream; -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; use librqbit_core::lengths::Lengths; use librqbit_core::peer_id::generate_peer_id; diff --git a/crates/librqbit/src/tracker_comms.rs b/crates/librqbit/src/tracker_comms.rs index 54292f04..e263be74 100644 --- a/crates/librqbit/src/tracker_comms.rs +++ b/crates/librqbit/src/tracker_comms.rs @@ -8,7 +8,7 @@ use std::{ str::FromStr, }; -use librqbit_core::id20::Id20; +use librqbit_core::hash_id::Id20; #[derive(Clone, Copy)] pub enum TrackerRequestEvent { @@ -207,10 +207,10 @@ mod tests { use super::*; #[test] fn test_serialize() { - let info_hash = Id20([ + let info_hash = Id20::new([ 1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ]); - let peer_id = Id20([ + let peer_id = Id20::new([ 1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ]); let request = TrackerRequest { diff --git a/crates/librqbit_core/src/id20.rs b/crates/librqbit_core/src/hash_id.rs similarity index 59% rename from crates/librqbit_core/src/id20.rs rename to crates/librqbit_core/src/hash_id.rs index 18c66d6d..1a27f978 100644 --- a/crates/librqbit_core/src/id20.rs +++ b/crates/librqbit_core/src/hash_id.rs @@ -2,33 +2,82 @@ use std::{cmp::Ordering, str::FromStr}; use serde::{Deserialize, Deserializer, Serialize}; -/// A 20-byte hash used throughout librqbit, for torrent info hashes, peer ids etc. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)] -pub struct Id20(pub [u8; 20]); +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct Id(pub [u8; N]); -impl FromStr for Id20 { - type Err = anyhow::Error; +impl Id { + pub fn new(from: [u8; N]) -> Id { + Id(from) + } - fn from_str(s: &str) -> Result { - let mut out = [0u8; 20]; - if s.len() != 40 { - anyhow::bail!("expected a hex string of length 40") - }; - hex::decode_to_slice(s, &mut out)?; - Ok(Id20(out)) + pub fn as_string(&self) -> String { + hex::encode(self.0) + } + + pub fn distance(&self, other: &Id) -> Id { + let mut xor = [0u8; N]; + for (idx, (s, o)) in self + .0 + .iter() + .copied() + .zip(other.0.iter().copied()) + .enumerate() + { + xor[idx] = s ^ o; + } + Id(xor) + } + pub fn get_bit(&self, bit: u8) -> bool { + let n = self.0[(bit / 8) as usize]; + let mask = 1 << (7 - bit % 8); + n & mask > 0 + } + + pub fn set_bit(&mut self, bit: u8, value: bool) { + let n = &mut self.0[(bit / 8) as usize]; + if value { + *n |= 1 << (7 - bit % 8) + } else { + let mask = !(1 << (7 - bit % 8)); + *n &= mask; + } + } + pub fn set_bits_range(&mut self, r: std::ops::Range, value: bool) { + for bit in r { + self.set_bit(bit, value) + } + } +} + +impl Default for Id { + fn default() -> Self { + Id([0; N]) } } -impl std::fmt::Debug for Id20 { +impl std::fmt::Debug for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for byte in self.0 { - write!(f, "{byte:02x?}")?; + write!(f, "{:02x?}", byte)?; } Ok(()) } } -impl Serialize for Id20 { +impl FromStr for Id { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut out = [0u8; N]; + if s.len() != N*2 { + anyhow::bail!("expected a hex string of length {}", N*2) + }; + hex::decode_to_slice(s, &mut out)?; + Ok(Id(out)) + } +} + +impl Serialize for Id { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -37,94 +86,67 @@ impl Serialize for Id20 { } } -impl<'de> Deserialize<'de> for Id20 { +impl<'de, const N: usize> Deserialize<'de> for Id { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - struct Visitor; - impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = Id20; + struct IdVisitor; + + impl<'de, const N: usize> serde::de::Visitor<'de> for IdVisitor { + type Value = Id; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a 20 byte slice or a 40 byte string") + formatter.write_str("a byte array of length ") + .and_then(|_| formatter.write_fmt(format_args!("{}", N))) } + fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { - if v.len() != 40 { + if v.len() != N * 2 { return Err(E::invalid_length(40, &self)); } - let mut out = [0u8; 20]; + let mut out = [0u8; N]; match hex::decode_to_slice(v, &mut out) { - Ok(_) => Ok(Id20(out)), + Ok(_) => Ok(Id(out)), Err(e) => Err(E::custom(e)), } } + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where E: serde::de::Error, { self.visit_bytes(v) } + fn visit_bytes(self, v: &[u8]) -> Result where E: serde::de::Error, { - if v.len() != 20 { - return Err(E::invalid_length(20, &self)); + if v.len() != N { + return Err(E::invalid_length(N, &self)); } - let mut buf = [0u8; 20]; + let mut buf = [0u8; N]; buf.copy_from_slice(v); - Ok(Id20(buf)) + Ok(Id(buf)) } } - deserializer.deserialize_any(Visitor {}) - } -} -impl Id20 { - pub fn as_string(&self) -> String { - hex::encode(self.0) - } - pub fn distance(&self, other: &Id20) -> Id20 { - let mut xor = [0u8; 20]; - for (idx, (s, o)) in self - .0 - .iter() - .copied() - .zip(other.0.iter().copied()) - .enumerate() - { - xor[idx] = s ^ o; - } - Id20(xor) - } - pub fn get_bit(&self, bit: u8) -> bool { - let n = self.0[(bit / 8) as usize]; - let mask = 1 << (7 - bit % 8); - n & mask > 0 + deserializer.deserialize_any(IdVisitor{}) } +} - pub fn set_bit(&mut self, bit: u8, value: bool) { - let n = &mut self.0[(bit / 8) as usize]; - if value { - *n |= 1 << (7 - bit % 8) - } else { - let mask = !(1 << (7 - bit % 8)); - *n &= mask; - } - } - pub fn set_bits_range(&mut self, r: std::ops::Range, value: bool) { - for bit in r { - self.set_bit(bit, value) - } +impl PartialOrd> for Id { + fn partial_cmp(&self, other: &Id) -> Option { + Some(self.cmp(other)) } } -impl Ord for Id20 { - fn cmp(&self, other: &Id20) -> Ordering { +impl Ord for Id { + fn cmp(&self, other: &Id) -> Ordering { for (s, o) in self.0.iter().copied().zip(other.0.iter().copied()) { match s.cmp(&o) { Ordering::Less => return Ordering::Less, @@ -136,23 +158,30 @@ impl Ord for Id20 { } } -impl PartialOrd for Id20 { - fn partial_cmp(&self, other: &Id20) -> Option { - Some(self.cmp(other)) - } -} +/// A 20-byte hash used throughout librqbit, for torrent info hashes, peer ids etc. +pub type Id20 = Id<20>; +/// A 32-byte hash used in Bittorrent V2, for torrent info hashes, piece hashing, etc. +pub type Id32 = Id<32>; #[cfg(test)] mod tests { - use super::Id20; + use std::str::FromStr; + use super::*; #[test] fn test_set_bit_range() { - let mut id = Id20([0u8; 20]); + let mut id = Id20::default(); id.set_bits_range(9..17, true); assert_eq!( id, - Id20([0, 127, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + Id20::new([0, 127, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) ) } -} + + #[test] + fn test_id32_from_str() { + let str = "06f04cc728bef957a658876ef807f0514e4d715392969998efef584d2c3e435e"; + let _ih = Id32::from_str(str).unwrap(); + } + +} \ No newline at end of file diff --git a/crates/librqbit_core/src/lib.rs b/crates/librqbit_core/src/lib.rs index 7a17daa2..6086598c 100644 --- a/crates/librqbit_core/src/lib.rs +++ b/crates/librqbit_core/src/lib.rs @@ -1,6 +1,6 @@ pub mod constants; pub mod directories; -pub mod id20; +pub mod hash_id; pub mod lengths; pub mod magnet; pub mod peer_id; diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 4ab8c5f5..477dbf69 100644 --- a/crates/librqbit_core/src/magnet.rs +++ b/crates/librqbit_core/src/magnet.rs @@ -2,41 +2,61 @@ use std::str::FromStr; use anyhow::Context; -use crate::id20::Id20; +use crate::hash_id::{Id20, Id32}; + /// A parsed magnet link. pub struct Magnet { - pub info_hash: Id20, + id20: Option, + id32: Option, pub trackers: Vec, } impl Magnet { + pub fn as_id20(&self) -> Option { + self.id20 + } + + pub fn as_id32(&self) -> Option { + self.id32 + } + /// Parse a magnet link. pub fn parse(url: &str) -> anyhow::Result { let url = url::Url::parse(url).context("magnet link must be a valid URL")?; if url.scheme() != "magnet" { anyhow::bail!("expected scheme magnet"); } - let mut info_hash: Option = None; + let mut info_hash_found = false; + let mut id20: Option = None; + let mut id32: Option = None; let mut trackers = Vec::::new(); for (key, value) in url.query_pairs() { match key.as_ref() { - "xt" => match value.as_ref().strip_prefix("urn:btih:") { - Some(infohash) => { - info_hash.replace(Id20::from_str(infohash)?); + "xt" => { + if let Some(ih) = value.as_ref().strip_prefix("urn:btih:") { + let i = Id20::from_str(ih)?; + id20.replace(i); + info_hash_found = true; + } else if let Some(ih) = value.as_ref().strip_prefix("urn:btmh:1220") { + let i = Id32::from_str(ih)?; + id32.replace(i); + info_hash_found = true; + } else { + anyhow::bail!("expected xt to start with btih or btmh"); } - None => anyhow::bail!("expected xt to start with urn:btih:"), }, "tr" => trackers.push(value.into()), _ => {} } } - match info_hash { - Some(info_hash) => Ok(Magnet { - info_hash, + match info_hash_found { + true => Ok(Magnet { + id20, + id32, trackers, }), - None => { + false => { anyhow::bail!("did not find infohash") } } @@ -45,15 +65,35 @@ impl Magnet { impl std::fmt::Display for Magnet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "magnet:?xt=urn:btih:{}&tr={}", - self.info_hash.as_string(), - self.trackers.join("&tr=") - ) + if let (Some(id20), Some(id32)) = (self.id20, self.id32) { + write!( + f, + "magnet:?xt=urn:btih:{}?xt=urn:btmh:1220{}&tr={}", + id20.as_string(), + id32.as_string(), + self.trackers.join("&tr=") + ) + } else if let Some(id20) = self.id20 { + write!( + f, + "magnet:?xt=urn:btih:{}&tr={}", + id20.as_string(), + self.trackers.join("&tr=") + ) + } else if let Some(id32) = self.id32 { + write!( + f, + "magnet:?xt=urn:btmh:1220{}&tr={}", + id32.as_string(), + self.trackers.join("&tr=") + ) + } else { + panic!("no infohash") + } } } + #[cfg(test)] mod tests { #[test] @@ -61,4 +101,16 @@ mod tests { let magnet = "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5&dn=rutor.info_%D0%92%D0%BE%D0%B9%D0%BD%D0%B0+%D0%B1%D1%83%D0%B4%D1%83%D1%89%D0%B5%D0%B3%D0%BE+%2F+The+Tomorrow+War+%282021%29+WEB-DLRip+%D0%BE%D1%82+MegaPeer+%7C+P+%7C+NewComers&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce"; dbg!(url::Url::parse(magnet).unwrap()); } + + #[test] + fn test_parse_magnet_v2() { + use super::Magnet; + use crate::magnet::Id32; + use std::str::FromStr; + let magnet = "magnet:?xt=urn:btmh:1220caf1e1c30e81cb361b9ee167c4aa64228a7fa4fa9f6105232b28ad099f3a302e&dn=bittorrent-v2-test +"; + let info_hash = Id32::from_str("caf1e1c30e81cb361b9ee167c4aa64228a7fa4fa9f6105232b28ad099f3a302e").unwrap(); + let m = Magnet::parse(&magnet).unwrap(); + assert!(m.as_id32() == Some(info_hash)); + } } diff --git a/crates/librqbit_core/src/peer_id.rs b/crates/librqbit_core/src/peer_id.rs index 77806444..3bf96ae3 100644 --- a/crates/librqbit_core/src/peer_id.rs +++ b/crates/librqbit_core/src/peer_id.rs @@ -1,4 +1,4 @@ -use crate::id20::Id20; +use crate::hash_id::Id20; #[derive(Debug)] pub enum AzureusStyleKind { @@ -55,5 +55,5 @@ pub fn generate_peer_id() -> Id20 { peer_id[..8].copy_from_slice(b"-rQ0001-"); - Id20(peer_id) + Id20::new(peer_id) } diff --git a/crates/librqbit_core/src/torrent_metainfo.rs b/crates/librqbit_core/src/torrent_metainfo.rs index 883df1f7..1a7711bc 100644 --- a/crates/librqbit_core/src/torrent_metainfo.rs +++ b/crates/librqbit_core/src/torrent_metainfo.rs @@ -7,7 +7,7 @@ use clone_to_owned::CloneToOwned; use itertools::Either; use serde::{Deserialize, Serialize}; -use crate::id20::Id20; +use crate::hash_id::Id20; pub type TorrentMetaV1Borrowed<'a> = TorrentMetaV1>; pub type TorrentMetaV1Owned = TorrentMetaV1; @@ -19,7 +19,7 @@ pub fn torrent_from_bytes<'de, ByteBuf: Deserialize<'de>>( let mut de = BencodeDeserializer::new_from_buf(buf); de.is_torrent_info = true; let mut t = TorrentMetaV1::deserialize(&mut de)?; - t.info_hash = Id20( + t.info_hash = Id20::new( de.torrent_info_digest .ok_or_else(|| anyhow::anyhow!("programming error"))?, ); diff --git a/crates/peer_binary_protocol/src/lib.rs b/crates/peer_binary_protocol/src/lib.rs index 4811ab40..f1128b13 100644 --- a/crates/peer_binary_protocol/src/lib.rs +++ b/crates/peer_binary_protocol/src/lib.rs @@ -8,7 +8,7 @@ use bincode::Options; use buffers::{ByteBuf, ByteString}; use byteorder::{ByteOrder, BE}; use clone_to_owned::CloneToOwned; -use librqbit_core::{constants::CHUNK_SIZE, id20::Id20, lengths::ChunkInfo}; +use librqbit_core::{constants::CHUNK_SIZE, hash_id::Id20, lengths::ChunkInfo}; use serde::{Deserialize, Serialize}; use self::extended::ExtendedMessage; @@ -596,10 +596,10 @@ mod tests { use super::*; #[test] fn test_handshake_serialize() { - let info_hash = Id20([ + let info_hash = Id20::new([ 1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ]); - let peer_id = Id20([ + let peer_id = Id20::new([ 1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ]); let mut buf = Vec::new();