diff --git a/crates/librqbit/src/api.rs b/crates/librqbit/src/api.rs index ec8a4f6..c9f90fe 100644 --- a/crates/librqbit/src/api.rs +++ b/crates/librqbit/src/api.rs @@ -209,10 +209,7 @@ impl Api { let mut r = TorrentDetailsResponse { id: Some(id), info_hash: mgr.shared().info_hash.as_string(), - name: mgr - .with_metadata(|r| r.info.name.as_ref().map(|n| n.to_string())) - .ok() - .flatten(), + name: mgr.name(), output_folder: mgr .shared() .options @@ -249,6 +246,7 @@ impl Api { Some(handle.id()), &info_hash, handle.metadata.load().as_ref().map(|r| &r.info), + handle.name().as_deref(), only_files.as_deref(), output_folder, ) @@ -383,6 +381,7 @@ impl Api { Some(id), &handle.info_hash(), handle.metadata.load().as_ref().map(|r| &r.info), + handle.name().as_deref(), handle.only_files().as_deref(), handle .shared() @@ -419,6 +418,7 @@ impl Api { None, &info_hash, Some(&info), + None, only_files.as_deref(), output_folder.to_string_lossy().into_owned().to_string(), ) @@ -429,6 +429,7 @@ impl Api { Some(id), &handle.info_hash(), handle.metadata.load().as_ref().map(|r| &r.info), + handle.name().as_deref(), handle.only_files().as_deref(), handle .shared() @@ -532,6 +533,7 @@ fn make_torrent_details( id: Option, info_hash: &Id20, info: Option<&TorrentMetaV1Info>, + name: Option<&str>, only_files: Option<&[usize]>, output_folder: String, ) -> Result { @@ -564,7 +566,9 @@ fn make_torrent_details( Ok(TorrentDetailsResponse { id, info_hash: info_hash.as_string(), - name: info.and_then(|i| i.name.as_ref().map(|b| b.to_string())), + name: name + .map(|s| s.to_owned()) + .or_else(|| info.and_then(|i| i.name.as_ref().map(|b| b.to_string()))), files: Some(files), output_folder, stats: None, diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 61a5401..4131c39 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -477,6 +477,7 @@ struct InternalAddResult { info_hash: Id20, metadata: Option, trackers: Vec, + name: Option, } impl Session { @@ -899,6 +900,7 @@ impl Session { info_hash, trackers: magnet.trackers, metadata: None, + name: magnet.name, } } other => { @@ -943,6 +945,7 @@ impl Session { torrent.info_bytes, )?), trackers, + name: None, } } }; @@ -956,6 +959,7 @@ impl Session { fn get_default_subfolder_for_torrent( &self, info: &TorrentMetaV1Info, + magnet_name: Option<&str>, ) -> anyhow::Result> { let files = info .iter_file_details()? @@ -964,11 +968,23 @@ impl Session { if files.len() < 2 { return Ok(None); } + fn check_valid(name: &str) -> anyhow::Result<()> { + if name.contains("/") || name.contains("\\") || name.contains("..") { + bail!("path traversal in torrent name detected") + } + Ok(()) + } + if let Some(name) = &info.name { let s = std::str::from_utf8(name.as_slice()).context("invalid UTF-8 in torrent name")?; + check_valid(s)?; return Ok(Some(PathBuf::from(s))); }; + if let Some(name) = magnet_name { + check_valid(name)?; + return Ok(Some(PathBuf::from(name))); + } // Let the subfolder name be the longest filename let longest = files .iter() @@ -989,6 +1005,7 @@ impl Session { info_hash, metadata, trackers, + name, } = add_res; let peer_stream_permanent = !opts.paused && !opts.list_only; @@ -1045,7 +1062,7 @@ impl Session { let output_folder = match (opts.output_folder, opts.sub_folder) { (None, None) => self.output_folder.join( - self.get_default_subfolder_for_torrent(&metadata.info)? + self.get_default_subfolder_for_torrent(&metadata.info, name.as_deref())? .unwrap_or_default(), ), (Some(o), None) => PathBuf::from(o), @@ -1118,6 +1135,7 @@ impl Session { }, connector: self.connector.clone(), session: Arc::downgrade(self), + magnet_name: name, }); let initializing = Arc::new(TorrentStateInitializing::new( diff --git a/crates/librqbit/src/torrent_state/mod.rs b/crates/librqbit/src/torrent_state/mod.rs index e559be5..d0454ce 100644 --- a/crates/librqbit/src/torrent_state/mod.rs +++ b/crates/librqbit/src/torrent_state/mod.rs @@ -141,6 +141,7 @@ pub struct TorrentMetadata { pub info_bytes: Bytes, pub lengths: Lengths, pub file_infos: FileInfos, + pub name: Option, } impl TorrentMetadata { @@ -162,12 +163,18 @@ impl TorrentMetadata { }) }) .collect::>>()?; + let name = info + .name + .as_ref() + .and_then(|n| std::str::from_utf8(n.as_ref()).ok()) + .map(|s| s.to_owned()); Ok(Self { info, torrent_bytes, info_bytes, lengths, file_infos, + name, }) } } @@ -188,6 +195,9 @@ pub struct ManagedTorrentShared { pub(crate) connector: Arc, pub(crate) storage_factory: BoxStorageFactory, pub(crate) session: Weak, + + // "dn" from magnet link + pub(crate) magnet_name: Option, } pub struct ManagedTorrent { @@ -204,6 +214,13 @@ impl ManagedTorrent { self.shared.id } + pub fn name(&self) -> Option { + if let Some(m) = &*self.metadata.load() { + return m.name.clone().or_else(|| self.shared.magnet_name.clone()); + } + self.shared.magnet_name.clone() + } + pub fn shared(&self) -> &ManagedTorrentShared { &self.shared } diff --git a/crates/librqbit_core/src/magnet.rs b/crates/librqbit_core/src/magnet.rs index 2a3d35a..fdd22c0 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, + pub name: Option, select_only: Option>, } @@ -29,6 +30,7 @@ impl Magnet { id20: Some(id20), id32: None, trackers, + name: None, select_only, } } @@ -40,6 +42,7 @@ impl Magnet { return Ok(Magnet { id20: Some(id20), id32: None, + name: None, trackers: vec![], select_only: None, }); @@ -52,6 +55,7 @@ impl Magnet { let mut info_hash_found = false; let mut id20: Option = None; let mut id32: Option = None; + let mut name: Option = None; let mut trackers = Vec::::new(); let mut files = Vec::::new(); for (key, value) in url.query_pairs() { @@ -70,6 +74,11 @@ impl Magnet { } } "tr" => trackers.push(value.into()), + "dn" => { + if !value.is_empty() { + name = Some(value.into_owned()) + } + } "so" => { // Process 'so' values, but silently ignore any which fail parsing for file_desc in value.split(',') { @@ -100,6 +109,7 @@ impl Magnet { id20, id32, trackers, + name, select_only: if files.is_empty() { None } else { Some(files) }, }), false => {