diff --git a/crates/bencode/src/serde_bencode_de.rs b/crates/bencode/src/serde_bencode_de.rs index 83bef6de..cc170bf6 100644 --- a/crates/bencode/src/serde_bencode_de.rs +++ b/crates/bencode/src/serde_bencode_de.rs @@ -218,14 +218,26 @@ impl<'de> serde::de::Deserializer<'de> for &mut BencodeDeserializer<'de> { } } - fn deserialize_bool(self, _visitor: V) -> Result + fn deserialize_bool(self, visitor: V) -> Result where V: serde::de::Visitor<'de>, { - Err( - Error::new_from_kind(ErrorKind::NotSupported("bencode doesn't support booleans")) - .set_context(self), - ) + if !self.buf.starts_with(b"i") { + return Err(Error::custom_with_de( + "expected bencode int to represent bool", + self, + )); + } + let value = self.parse_integer()?; + if value > 1 { + return Err(Error::custom_with_de( + format!("expected 0 or 1 for boolean, but got {value}"), + self, + )); + } + visitor + .visit_bool(value == 1) + .map_err(|e: Self::Error| e.set_context(self)) } fn deserialize_i8(self, visitor: V) -> Result diff --git a/crates/bencode/src/serde_bencode_ser.rs b/crates/bencode/src/serde_bencode_ser.rs index 98a4a27e..8e3859b9 100644 --- a/crates/bencode/src/serde_bencode_ser.rs +++ b/crates/bencode/src/serde_bencode_ser.rs @@ -218,11 +218,8 @@ impl<'ser, W: std::io::Write> Serializer for &'ser mut BencodeSerializer { type SerializeStruct = SerializeStruct<'ser, W>; type SerializeStructVariant = Impossible<(), SerError>; - fn serialize_bool(self, _: bool) -> Result { - Err(SerError::custom_with_ser( - "bencode doesn't support booleans", - self, - )) + fn serialize_bool(self, value: bool) -> Result { + self.write_number(if value { 1 } else { 0 }) } fn serialize_i8(self, v: i8) -> Result { diff --git a/crates/librqbit/src/create_torrent_file.rs b/crates/librqbit/src/create_torrent_file.rs index 18ea5dc7..6ee895e4 100644 --- a/crates/librqbit/src/create_torrent_file.rs +++ b/crates/librqbit/src/create_torrent_file.rs @@ -163,6 +163,7 @@ async fn create_torrent_raw<'a>( attr: None, sha1: None, symlink_path: None, + private: false, }) } diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index a22b53fc..5acc511e 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -1008,6 +1008,8 @@ impl Session { name, } = add_res; + let private = metadata.as_ref().map_or(false, |m| m.info.private); + let make_peer_rx = || { self.make_peer_rx( info_hash, @@ -1015,6 +1017,7 @@ impl Session { !opts.paused && !opts.list_only, opts.force_tracker_interval, opts.initial_peers.clone().unwrap_or_default(), + private, ) .context("error creating peer stream") }; @@ -1284,12 +1287,14 @@ impl Session { t: &Arc, announce: bool, ) -> anyhow::Result { + let is_private = t.with_metadata(|m| m.info.private).unwrap_or(false); self.make_peer_rx( t.info_hash(), t.shared().trackers.iter().cloned().collect(), announce, t.shared().options.force_tracker_interval, t.shared().options.initial_peers.clone(), + is_private, )? .context("no peer source") } @@ -1298,17 +1303,26 @@ impl Session { fn make_peer_rx( self: &Arc, info_hash: Id20, - trackers: Vec, + mut trackers: Vec, announce: bool, force_tracker_interval: Option, initial_peers: Vec, + is_private: bool, ) -> anyhow::Result> { let announce_port = if announce { self.tcp_listen_port } else { None }; - let dht_rx = self - .dht - .as_ref() - .map(|dht| dht.get_peers(info_hash, announce_port)) - .transpose()?; + let dht_rx = if is_private { + None + } else { + self.dht + .as_ref() + .map(|dht| dht.get_peers(info_hash, announce_port)) + .transpose()? + }; + + if is_private && trackers.len() > 1 { + warn!("private trackers are not fully implemented, so using only the first tracker"); + trackers.resize_with(1, Default::default); + } let tracker_rx_stats = PeerRxTorrentInfo { info_hash, diff --git a/crates/librqbit/src/tests/test_util.rs b/crates/librqbit/src/tests/test_util.rs index 9df9866e..1130b8ca 100644 --- a/crates/librqbit/src/tests/test_util.rs +++ b/crates/librqbit/src/tests/test_util.rs @@ -134,6 +134,7 @@ async fn debug_server() -> anyhow::Result<()> { Ok(()) } +#[allow(dead_code)] pub fn spawn_debug_server() -> tokio::task::JoinHandle> { tokio::spawn(debug_server()) } diff --git a/crates/librqbit/src/upnp_server_adapter.rs b/crates/librqbit/src/upnp_server_adapter.rs index 1885a6ec..b32e571a 100644 --- a/crates/librqbit/src/upnp_server_adapter.rs +++ b/crates/librqbit/src/upnp_server_adapter.rs @@ -430,6 +430,7 @@ mod tests { attr: None, sha1: None, symlink_path: None, + private: false, }, comment: None, created_by: None, diff --git a/crates/librqbit_core/src/resources/test/private.torrent b/crates/librqbit_core/src/resources/test/private.torrent new file mode 100644 index 00000000..f737e4c3 --- /dev/null +++ b/crates/librqbit_core/src/resources/test/private.torrent @@ -0,0 +1 @@ +d10:created by20:qBittorrent v4.4.3.113:creation datei1736784212e4:infod6:lengthi2e4:name7:private12:piece lengthi16384e6:pieces20:åúDò³µS¶s`Ð}]‘ÿ^7:privatei1eee \ No newline at end of file diff --git a/crates/librqbit_core/src/torrent_metainfo.rs b/crates/librqbit_core/src/torrent_metainfo.rs index 80db56ae..da546663 100644 --- a/crates/librqbit_core/src/torrent_metainfo.rs +++ b/crates/librqbit_core/src/torrent_metainfo.rs @@ -50,6 +50,10 @@ pub fn torrent_from_bytes<'de, BufType: Deserialize<'de> + From<&'de [u8]>>( torrent_from_bytes_ext(buf).map(|r| r.meta) } +fn is_false(b: &bool) -> bool { + !*b +} + /// A parsed .torrent file. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct TorrentMetaV1 { @@ -89,7 +93,7 @@ impl TorrentMetaV1 { } /// Main torrent information, shared by .torrent files and magnet link contents. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct TorrentMetaV1Info { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, @@ -117,6 +121,9 @@ pub struct TorrentMetaV1Info { // Multi-file mode #[serde(skip_serializing_if = "Option::is_none")] pub files: Option>>, + + #[serde(skip_serializing_if = "is_false", default)] + pub private: bool, } #[derive(Clone, Copy)] @@ -377,6 +384,7 @@ where attr: self.attr.clone_to_owned(within_buffer), sha1: self.sha1.clone_to_owned(within_buffer), symlink_path: self.symlink_path.clone_to_owned(within_buffer), + private: self.private, } } } @@ -405,45 +413,28 @@ where #[cfg(test)] mod tests { - use std::io::Read; + use bencode::BencodeValue; use super::*; - const TORRENT_FILENAME: &str = "../librqbit/resources/ubuntu-21.04-desktop-amd64.iso.torrent"; + const TORRENT_BYTES: &[u8] = + include_bytes!("../../librqbit/resources/ubuntu-21.04-desktop-amd64.iso.torrent"); #[test] fn test_deserialize_torrent_owned() { - let mut buf = Vec::new(); - std::fs::File::open(TORRENT_FILENAME) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - - let torrent: TorrentMetaV1Owned = torrent_from_bytes(&buf).unwrap(); + let torrent: TorrentMetaV1Owned = torrent_from_bytes(TORRENT_BYTES).unwrap(); dbg!(torrent); } #[test] fn test_deserialize_torrent_borrowed() { - let mut buf = Vec::new(); - std::fs::File::open(TORRENT_FILENAME) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - - let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(&buf).unwrap(); + let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(TORRENT_BYTES).unwrap(); dbg!(torrent); } #[test] fn test_deserialize_torrent_with_info_hash() { - let mut buf = Vec::new(); - std::fs::File::open(TORRENT_FILENAME) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - - let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(&buf).unwrap(); + let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(TORRENT_BYTES).unwrap(); assert_eq!( torrent.info_hash.as_string(), "64a980abe6e448226bb930ba061592e44c3781a1" @@ -452,13 +443,7 @@ mod tests { #[test] fn test_serialize_then_deserialize_bencode() { - let mut buf = Vec::new(); - std::fs::File::open(TORRENT_FILENAME) - .unwrap() - .read_to_end(&mut buf) - .unwrap(); - - let torrent: TorrentMetaV1Info = torrent_from_bytes(&buf).unwrap().info; + let torrent: TorrentMetaV1Info = torrent_from_bytes(TORRENT_BYTES).unwrap().info; let mut writer = Vec::new(); bencode::bencode_serialize_to_writer(&torrent, &mut writer).unwrap(); let deserialized = TorrentMetaV1Info::::deserialize( @@ -468,4 +453,42 @@ mod tests { assert_eq!(torrent, deserialized); } + + #[test] + fn test_private_serialize_deserialize() { + for private in [false, true] { + let info: TorrentMetaV1Info = TorrentMetaV1Info { + private, + ..Default::default() + }; + let mut buf = Vec::new(); + bencode::bencode_serialize_to_writer(&info, &mut buf).unwrap(); + + let deserialized = TorrentMetaV1Info::::deserialize( + &mut BencodeDeserializer::new_from_buf(&buf), + ) + .unwrap(); + assert_eq!(info.private, deserialized.private); + + let deserialized_dyn = ::bencode::dyn_from_bytes::(&buf).unwrap(); + let hm = match deserialized_dyn { + bencode::BencodeValue::Dict(hm) => hm, + _ => panic!("expected dict"), + }; + match (private, hm.get(&ByteBuf(b"private"))) { + (true, Some(BencodeValue::Integer(1))) => {} + (false, None) => {} + (_, v) => { + panic!("unexpected value for \"private\": {v:?}") + } + } + } + } + + #[test] + fn test_private_real_torrent() { + let buf = include_bytes!("resources/test/private.torrent"); + let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(buf).unwrap(); + assert!(torrent.info.private); + } }