diff --git a/crates/bencode/src/lib.rs b/crates/bencode/src/lib.rs index 2753a6a..1389395 100644 --- a/crates/bencode/src/lib.rs +++ b/crates/bencode/src/lib.rs @@ -1,4 +1,5 @@ mod bencode_value; +pub mod raw_value; mod serde_bencode_de; mod serde_bencode_ser; diff --git a/crates/bencode/src/raw_value.rs b/crates/bencode/src/raw_value.rs new file mode 100644 index 0000000..fbeceb8 --- /dev/null +++ b/crates/bencode/src/raw_value.rs @@ -0,0 +1,28 @@ +use serde::Serialize; + +pub struct RawValue(pub T); + +pub(crate) const TAG: &str = "::librqbit_bencode::RawValue"; + +impl Serialize for RawValue +where + T: AsRef<[u8]>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + struct Wrapper<'a>(&'a [u8]); + + impl<'a> Serialize for Wrapper<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.0) + } + } + + serializer.serialize_newtype_struct(TAG, &Wrapper(self.0.as_ref())) + } +} diff --git a/crates/bencode/src/serde_bencode_de.rs b/crates/bencode/src/serde_bencode_de.rs index 4299367..b6f3a47 100644 --- a/crates/bencode/src/serde_bencode_de.rs +++ b/crates/bencode/src/serde_bencode_de.rs @@ -10,6 +10,7 @@ pub struct BencodeDeserializer<'de> { // This is a f**ing hack pub is_torrent_info: bool, pub torrent_info_digest: Option<[u8; 20]>, + pub torrent_info_bytes: Option<&'de [u8]>, } impl<'de> BencodeDeserializer<'de> { @@ -20,6 +21,7 @@ impl<'de> BencodeDeserializer<'de> { parsing_key: false, is_torrent_info: false, torrent_info_digest: None, + torrent_info_bytes: None, } } pub fn into_remaining(self) -> &'de [u8] { @@ -542,9 +544,11 @@ impl<'a, 'de> serde::de::MapAccess<'de> for MapAccess<'a, 'de> { if self.de.is_torrent_info && self.de.field_context.as_slice() == [ByteBuf(b"info")] { let len = self.de.buf.as_ptr() as usize - buf_before.as_ptr() as usize; let mut hash = Sha1::new(); - hash.update(&buf_before[..len]); + let torrent_info_bytes = &buf_before[..len]; + hash.update(torrent_info_bytes); let digest = hash.finish(); - self.de.torrent_info_digest = Some(digest) + self.de.torrent_info_digest = Some(digest); + self.de.torrent_info_bytes = Some(torrent_info_bytes); } self.de.field_context.pop(); Ok(value) diff --git a/crates/bencode/src/serde_bencode_ser.rs b/crates/bencode/src/serde_bencode_ser.rs index 78202ea..0618b99 100644 --- a/crates/bencode/src/serde_bencode_ser.rs +++ b/crates/bencode/src/serde_bencode_ser.rs @@ -328,12 +328,18 @@ impl<'ser, W: std::io::Write> Serializer for &'ser mut BencodeSerializer { fn serialize_newtype_struct( self, - _name: &'static str, - _value: &T, + name: &'static str, + value: &T, ) -> Result where T: ?Sized + serde::Serialize, { + if name == crate::raw_value::TAG { + self.hack_no_bytestring_prefix = true; + value.serialize(&mut *self)?; + self.hack_no_bytestring_prefix = false; + return Ok(()); + } Err(SerError::custom_with_ser( "bencode doesn't support newtype structs", self, diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 4aa1b1a..cef29a2 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -502,13 +502,23 @@ async fn create_tcp_listener( bail!("no free TCP ports in range {port_range:?}"); } -fn torrent_file_from_info_and_bytes( - info: &TorrentMetaV1Info, - info_hash: &Id20, - info_bytes: &[u8], - trackers: &[String], -) -> Bytes { - todo!() +fn torrent_file_from_info_bytes(info_bytes: &[u8], trackers: &[String]) -> anyhow::Result { + #[derive(Serialize)] + struct Tmp<'a> { + announce: &'a str, + #[serde(rename = "announce-list")] + announce_list: &'a [&'a [String]], + info: bencode::raw_value::RawValue<&'a [u8]>, + } + + let mut w = Vec::new(); + let v = Tmp { + info: bencode::raw_value::RawValue(info_bytes), + announce: trackers.first().map(|s| s.as_str()).unwrap_or(""), + announce_list: &[trackers], + }; + bencode_serialize_to_writer(&v, &mut w)?; + Ok(w.into()) } pub(crate) struct CheckedIncomingConnection { @@ -1003,12 +1013,10 @@ impl Session { let trackers = magnet.trackers.into_iter().unique().collect_vec(); InternalAddResult { info_hash, - torrent_bytes: torrent_file_from_info_and_bytes( - &info, - &info_hash, + torrent_bytes: torrent_file_from_info_bytes( &info_bytes, &trackers, - ), + )?, info, trackers, peer_rx: Some(rx), @@ -1427,3 +1435,46 @@ impl tracker_comms::TorrentStatsProvider for PeerRxTorrentInfo { } } } + +#[cfg(test)] +mod tests { + use std::io::Write; + + use buffers::ByteBuf; + use itertools::Itertools; + use librqbit_core::torrent_metainfo::{torrent_from_bytes_ext, TorrentMetaV1}; + + use super::torrent_file_from_info_bytes; + + #[test] + fn test_torrent_file_from_info_and_bytes() { + fn get_trackers(info: &TorrentMetaV1) -> Vec { + info.iter_announce() + .filter_map(|t| std::str::from_utf8(t.as_ref()).ok().map(|t| t.to_owned())) + .collect_vec() + } + + let orig_full_torrent = + include_bytes!("../resources/ubuntu-21.04-desktop-amd64.iso.torrent"); + let parsed = torrent_from_bytes_ext::(&orig_full_torrent[..]).unwrap(); + let parsed_trackers = get_trackers(&parsed.meta); + + let generated_torrent = + torrent_file_from_info_bytes(parsed.info_bytes.as_ref(), &parsed_trackers).unwrap(); + { + let mut f = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open("/tmp/generated") + .unwrap(); + f.write_all(&generated_torrent).unwrap(); + } + let generated_parsed = + torrent_from_bytes_ext::(generated_torrent.as_ref()).unwrap(); + assert_eq!(parsed.meta.info_hash, generated_parsed.meta.info_hash); + assert_eq!(parsed.meta.info, generated_parsed.meta.info); + assert_eq!(parsed.info_bytes, generated_parsed.info_bytes); + assert_eq!(parsed_trackers, get_trackers(&generated_parsed.meta)); + } +} diff --git a/crates/librqbit_core/src/torrent_metainfo.rs b/crates/librqbit_core/src/torrent_metainfo.rs index 9933f67..0ca37f6 100644 --- a/crates/librqbit_core/src/torrent_metainfo.rs +++ b/crates/librqbit_core/src/torrent_metainfo.rs @@ -12,18 +12,37 @@ use crate::{hash_id::Id20, lengths::Lengths}; pub type TorrentMetaV1Borrowed<'a> = TorrentMetaV1>; pub type TorrentMetaV1Owned = TorrentMetaV1; -/// Parse torrent metainfo from bytes. -pub fn torrent_from_bytes<'de, BufType: Deserialize<'de>>( +pub struct ParsedTorrent { + /// The parsed torrent. + pub meta: TorrentMetaV1, + + /// The raw bytes of the torrent's "info" dict. + pub info_bytes: BufType, +} + +/// Parse torrent metainfo from bytes (includes additional fields). +pub fn torrent_from_bytes_ext<'de, BufType: Deserialize<'de> + From<&'de [u8]>>( buf: &'de [u8], -) -> anyhow::Result> { +) -> anyhow::Result> { let mut de = BencodeDeserializer::new_from_buf(buf); de.is_torrent_info = true; let mut t = TorrentMetaV1::deserialize(&mut de)?; - t.info_hash = Id20::new( - de.torrent_info_digest - .ok_or_else(|| anyhow::anyhow!("programming error"))?, - ); - Ok(t) + let (digest, info_bytes) = match (de.torrent_info_digest, de.torrent_info_bytes) { + (Some(digest), Some(info_bytes)) => (digest, info_bytes), + _ => anyhow::bail!("programming error"), + }; + t.info_hash = Id20::new(digest); + Ok(ParsedTorrent { + meta: t, + info_bytes: BufType::from(info_bytes), + }) +} + +/// Parse torrent metainfo from bytes. +pub fn torrent_from_bytes<'de, BufType: Deserialize<'de> + From<&'de [u8]>>( + buf: &'de [u8], +) -> anyhow::Result> { + torrent_from_bytes_ext(buf).map(|r| r.meta) } /// A parsed .torrent file.