From aa0c287fe5e259c732faceeca4dd2b9146faf043 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 1 Oct 2024 12:59:45 -0500 Subject: [PATCH 1/5] feat: implement BEP-53 support --- crates/librqbit/src/session.rs | 5 +- .../librqbit/src/session_persistence/mod.rs | 2 +- crates/librqbit/src/tests/e2e.rs | 2 +- crates/librqbit_core/src/magnet.rs | 54 +++++++++++++++++-- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 8c2b749..95341d3 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -868,7 +868,7 @@ impl Session { ) -> BoxFuture<'a, anyhow::Result> { async move { // Magnet links are different in that we first need to discover the metadata. - let opts = opts.unwrap_or_default(); + let mut opts = opts.unwrap_or_default(); let paused = opts.list_only || opts.paused; @@ -885,6 +885,9 @@ impl Session { let info_hash = magnet .as_id20() .context("magnet link didn't contain a BTv1 infohash")?; + if let Some(so) = magnet.get_select_only() { + opts.only_files = Some(so); + } let peer_rx = self.make_peer_rx( info_hash, diff --git a/crates/librqbit/src/session_persistence/mod.rs b/crates/librqbit/src/session_persistence/mod.rs index f1459f8..952e04f 100644 --- a/crates/librqbit/src/session_persistence/mod.rs +++ b/crates/librqbit/src/session_persistence/mod.rs @@ -41,7 +41,7 @@ impl SerializedTorrent { AddTorrent::TorrentFileBytes(self.torrent_bytes) } else { let magnet = - Magnet::from_id20(self.info_hash, self.trackers.into_iter().collect()).to_string(); + Magnet::from_id20(self.info_hash, self.trackers.into_iter().collect(), self.only_files.clone()).to_string(); AddTorrent::from_url(magnet) }; diff --git a/crates/librqbit/src/tests/e2e.rs b/crates/librqbit/src/tests/e2e.rs index 3b8eda0..b83b680 100644 --- a/crates/librqbit/src/tests/e2e.rs +++ b/crates/librqbit/src/tests/e2e.rs @@ -196,7 +196,7 @@ async fn _test_e2e_download(drop_checks: &DropChecks) { .and_then(|v| v.parse().ok()) .unwrap_or(1usize); - let magnet = Magnet::from_id20(torrent_file.info_hash(), Vec::new()).to_string(); + let magnet = Magnet::from_id20(torrent_file.info_hash(), Vec::new(), None).to_string(); // 3. Start a client with the initial peers, and download the file. for _ in 0..client_iters { diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 203f260..03fed11 100644 --- a/crates/librqbit_core/src/magnet.rs +++ b/crates/librqbit_core/src/magnet.rs @@ -9,6 +9,7 @@ pub struct Magnet { id20: Option, id32: Option, pub trackers: Vec, + select_only: Option> } impl Magnet { @@ -19,12 +20,16 @@ impl Magnet { pub fn as_id32(&self) -> Option { self.id32 } + pub fn get_select_only(&self) -> Option> { + self.select_only.clone() + } - pub fn from_id20(id20: Id20, trackers: Vec) -> Self { + pub fn from_id20(id20: Id20, trackers: Vec, select_only: Option> ) -> Self { Self { id20: Some(id20), id32: None, trackers, + select_only, } } @@ -38,6 +43,7 @@ impl Magnet { let mut id20: Option = None; let mut id32: Option = None; let mut trackers = Vec::::new(); + let mut files = Vec::::new(); for (key, value) in url.query_pairs() { match key.as_ref() { "xt" => { @@ -54,6 +60,26 @@ impl Magnet { } } "tr" => trackers.push(value.into()), + "so" => { + for file_desc in value.split(',') { + if file_desc.is_empty() { + continue; + } + // Handling ranges of file indices + if let Some((start, end)) = file_desc.split_once('-') { + let start_idx: usize = start.parse()?; + let end_idx: usize = end.parse()?; + if start_idx >= end_idx { + anyhow::bail!("range start must be less than range end"); + } + files.extend(start_idx..=end_idx); + } else { + // Handling single file index + let idx: usize = file_desc.parse()?; + files.push(idx); + } + } + } _ => {} } } @@ -62,6 +88,10 @@ impl Magnet { id20, id32, trackers, + select_only: match files.is_empty() { + true => None, + false => Some(files), + }, }), false => { anyhow::bail!("did not find infohash") @@ -97,6 +127,18 @@ impl std::fmt::Display for Magnet { write_ampersand(f)?; write!(f, "tr={tracker}")?; } + if let Some(select_only) = &self.select_only { + if !select_only.is_empty() { + write_ampersand(f)?; + write!(f, "so=")?; + for (index, file) in select_only.iter().enumerate() { + if index > 0 { + write!(f, ",")?; // Add a comma before all but the first index + } + write!(f, "{}", file)?; + } + } + } Ok(()) } } @@ -133,13 +175,19 @@ mod tests { fn test_magnet_to_string() { let id20 = Id20::from_str("a621779b5e3d486e127c3efbca9b6f8d135f52e5").unwrap(); assert_eq!( - &Magnet::from_id20(id20, Default::default()).to_string(), + &Magnet::from_id20(id20, Default::default(), None).to_string(), "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5" ); assert_eq!( - &Magnet::from_id20(id20, vec!["foo".to_string(), "bar".to_string()]).to_string(), + &Magnet::from_id20(id20, vec!["foo".to_string(), "bar".to_string()], None).to_string(), "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5&tr=foo&tr=bar" ); + + assert_eq!( + &Magnet::from_id20(id20, Default::default(), Some(vec![1,2,3])).to_string(), + "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5&so=1,2,3" + ); + } } From 1b6e7edb6b7da9351fc6b81ed774e5ed0e25979f Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 2 Oct 2024 13:42:59 -0500 Subject: [PATCH 2/5] fixup: allow 'so' validation to fail, and just continue --- crates/librqbit_core/src/magnet.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 03fed11..5517327 100644 --- a/crates/librqbit_core/src/magnet.rs +++ b/crates/librqbit_core/src/magnet.rs @@ -61,22 +61,24 @@ impl Magnet { } "tr" => trackers.push(value.into()), "so" => { + // Process 'so' values, but silently ignore any which fail parsing for file_desc in value.split(',') { if file_desc.is_empty() { continue; } // Handling ranges of file indices if let Some((start, end)) = file_desc.split_once('-') { - let start_idx: usize = start.parse()?; - let end_idx: usize = end.parse()?; - if start_idx >= end_idx { - anyhow::bail!("range start must be less than range end"); + let maybe_start_idx: Result = start.parse(); + let maybe_end_idx: Result = end.parse(); + if let (Ok(start_idx), Ok(end_idx)) = (maybe_start_idx, maybe_end_idx) { + files.extend(start_idx..=end_idx); } - files.extend(start_idx..=end_idx); } else { // Handling single file index - let idx: usize = file_desc.parse()?; - files.push(idx); + let idx = file_desc.parse(); + if let Ok(idx) = idx { + files.push(idx); + } } } } From d480b14beaf83295f9ad5898f93875100d6eefad Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 2 Oct 2024 13:50:48 -0500 Subject: [PATCH 3/5] chore: run `cargo fmt` --- crates/librqbit/src/session_persistence/mod.rs | 8 ++++++-- crates/librqbit_core/src/magnet.rs | 7 +++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/librqbit/src/session_persistence/mod.rs b/crates/librqbit/src/session_persistence/mod.rs index 952e04f..ef62e36 100644 --- a/crates/librqbit/src/session_persistence/mod.rs +++ b/crates/librqbit/src/session_persistence/mod.rs @@ -40,8 +40,12 @@ impl SerializedTorrent { let add_torrent = if !self.torrent_bytes.is_empty() { AddTorrent::TorrentFileBytes(self.torrent_bytes) } else { - let magnet = - Magnet::from_id20(self.info_hash, self.trackers.into_iter().collect(), self.only_files.clone()).to_string(); + let magnet = Magnet::from_id20( + self.info_hash, + self.trackers.into_iter().collect(), + self.only_files.clone(), + ) + .to_string(); AddTorrent::from_url(magnet) }; diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 5517327..5ba577c 100644 --- a/crates/librqbit_core/src/magnet.rs +++ b/crates/librqbit_core/src/magnet.rs @@ -9,7 +9,7 @@ pub struct Magnet { id20: Option, id32: Option, pub trackers: Vec, - select_only: Option> + select_only: Option>, } impl Magnet { @@ -24,7 +24,7 @@ impl Magnet { self.select_only.clone() } - pub fn from_id20(id20: Id20, trackers: Vec, select_only: Option> ) -> Self { + pub fn from_id20(id20: Id20, trackers: Vec, select_only: Option>) -> Self { Self { id20: Some(id20), id32: None, @@ -187,9 +187,8 @@ mod tests { ); assert_eq!( - &Magnet::from_id20(id20, Default::default(), Some(vec![1,2,3])).to_string(), + &Magnet::from_id20(id20, Default::default(), Some(vec![1, 2, 3])).to_string(), "magnet:?xt=urn:btih:a621779b5e3d486e127c3efbca9b6f8d135f52e5&so=1,2,3" ); - } } From db420f3a521c77a4003bb423d3cd7a67a25253e4 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 2 Oct 2024 14:33:40 -0500 Subject: [PATCH 4/5] fixup: use if instead of match --- crates/librqbit_core/src/magnet.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 5ba577c..22ca5aa 100644 --- a/crates/librqbit_core/src/magnet.rs +++ b/crates/librqbit_core/src/magnet.rs @@ -90,9 +90,10 @@ impl Magnet { id20, id32, trackers, - select_only: match files.is_empty() { - true => None, - false => Some(files), + select_only: if files.is_empty() { + None + } else { + Some(files) }, }), false => { From 57db99e9b88cdcec61093113fb371d8182a36b99 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 2 Oct 2024 14:45:49 -0500 Subject: [PATCH 5/5] fixup: only set only_files if it was none --- crates/librqbit/src/session.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 95341d3..14729e3 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -886,7 +886,10 @@ impl Session { .as_id20() .context("magnet link didn't contain a BTv1 infohash")?; if let Some(so) = magnet.get_select_only() { - opts.only_files = Some(so); + // Only overwrite opts.only_files if user didn't specify + if opts.only_files.is_none() { + opts.only_files = Some(so); + } } let peer_rx = self.make_peer_rx(