Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 47 additions & 37 deletions crates/libtortillas/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
//!
//! // Add a torrent from a magnet URI
//! let torrent = engine
//! .add_torrent("magnet:?xt=urn:btih:...")
//! .add_torrent(TorrentSource::magnet("magnet:?xt=urn:btih:..."))
//! .await
//! .expect("Failed to add torrent");
//!
Expand All @@ -36,6 +36,7 @@

mod actor;
mod messages;
mod source;

use std::{net::SocketAddr, path::PathBuf};

Expand All @@ -44,12 +45,11 @@ use bon;
use kameo::actor::{ActorRef, Spawn};
pub(crate) use messages::*;
use serde::{Deserialize, Serialize};
use tracing::error;
pub use source::*;

use self::commands::{CreateTorrent, ExportEngine, StartAll};
use crate::{
errors::EngineError,
metainfo::{MetaInfo, TorrentFile},
peer::PeerId,
settings::Settings,
torrent::{PieceStorageStrategy, Torrent, TorrentExport},
Expand Down Expand Up @@ -205,15 +205,8 @@ impl Engine {
/// automatically contacts trackers and connects to peers. The spawned
/// [Torrent Actor](Torrent) will be controlled by the [Engine].
///
/// This function accepts the following as input:
/// - A remote URL to a torrent file over HTTP/HTTPS
/// - The path, either absolute or relative, to a local torrent file
/// - A magnet URI
///
/// If the inputted value is a remote url to a torrent file, this function
/// requests the bytes and deserializes them into a [TorrentFile]. If
/// it isn't, we assume that it is either a magnet URI or a path to a
/// torrent file, and pass the string to [MetaInfo::new].
/// This function accepts a typed [`TorrentSource`] so frontends can pass
/// explicit user intent instead of relying on string-prefix detection.
///
///
/// # Examples
Expand All @@ -227,7 +220,7 @@ impl Engine {
/// let engine = Engine::default();
/// let torrent_link = "https://example.com/example.torrent";
/// let torrent = engine
/// .add_torrent(torrent_link)
/// .add_torrent(TorrentSource::remote_torrent_url(torrent_link))
/// .await
/// .expect("Failed to add torrent");
///
Expand All @@ -244,36 +237,15 @@ impl Engine {
/// let engine = Engine::default();
/// let magnet_uri = "magnet:?xt=?????";
/// let torrent = engine
/// .add_torrent(magnet_uri)
/// .add_torrent(TorrentSource::magnet(magnet_uri))
/// .await
/// .expect("Failed to add torrent");
///
/// println!("Started torrenting: {}", torrent.key());
/// }
/// ```
pub async fn add_torrent(&self, metainfo: impl ToString) -> Result<Torrent, EngineError> {
let metainfo = metainfo.to_string();
// File paths should either start with "/" or "./", and magnet URIs start
// with "magnet:", so a check like this should be entirely appropriate.
let metainfo = if metainfo.starts_with("http") {
let torrent_file_bytes = reqwest::get(&metainfo)
.await
.map_err(EngineError::MetaInfoFetchError)?
.bytes()
.await
.map_err(EngineError::MetaInfoFetchError)?;
let torrent_file = TorrentFile::parse(&torrent_file_bytes);
torrent_file.map_err(|_| {
error!(remote_url = metainfo);
EngineError::MetaInfoDeserializeError
})?
} else {
MetaInfo::new(metainfo.clone()).await.map_err(|_| {
error!(magnet_uri_or_file = metainfo);
EngineError::MetaInfoDeserializeError
})?
};

pub async fn add_torrent(&self, source: TorrentSource) -> Result<Torrent, EngineError> {
let metainfo = source.into_metainfo().await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify remote torrent loading is bounded by an explicit reqwest timeout.
rg -n -C4 'reqwest::get|Client::builder|timeout\(' crates/libtortillas/src/engine/source.rs

Repository: artrixdotdev/tortillas

Length of output: 456


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the loader path and the remote fetch implementation around the reported lines.
sed -n '220,270p' crates/libtortillas/src/engine/mod.rs
printf '\n---\n'
sed -n '100,150p' crates/libtortillas/src/engine/source.rs
printf '\n---\n'
# Look for any timeout/cancellation applied elsewhere in the engine path.
rg -n 'timeout\(|reqwest::Client::builder|reqwest::get|into_metainfo\(' crates/libtortillas/src/engine

Repository: artrixdotdev/tortillas

Length of output: 4305


Bound remote torrent fetches with a client timeout. source.into_metainfo() can reach metainfo_from_remote_url, which still uses bare reqwest::get without any timeout. A stalled endpoint can leave add_torrent pending indefinitely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libtortillas/src/engine/mod.rs` at line 248, The remote metainfo fetch
path used by add_torrent is unbounded because source.into_metainfo() can reach
metainfo_from_remote_url, which still relies on an un-timed reqwest::get call.
Update the metainfo fetch flow in the engine/mod.rs path so the request is
executed with the existing client timeout configuration, and make
metainfo_from_remote_url use a timeout-aware client or request builder instead
of bare reqwest::get. Keep the fix localized around source.into_metainfo(),
metainfo_from_remote_url, and the add_torrent call chain so stalled endpoints
fail promptly instead of hanging indefinitely.

let info_hash = metainfo.info_hash()?;

let torrent_ref = self
Expand Down Expand Up @@ -320,3 +292,41 @@ impl Default for Engine {
pub struct EngineExport {
pub torrents: Vec<TorrentExport>,
}

#[cfg(test)]
mod tests {
use crate::{
engine::{Engine, TorrentSource},
errors::EngineError,
testing::{BIG_BUCK_BUNNY_INFO_HASH, BIG_BUCK_BUNNY_TORRENT_FILE, torrent_fixture_path},
};

#[tokio::test]
async fn engine_when_torrent_source_is_file_path_then_adds_torrent() {
let engine = Engine::builder().autostart(false).build();
let source =
TorrentSource::torrent_file_path(torrent_fixture_path(BIG_BUCK_BUNNY_TORRENT_FILE));

let torrent = engine.add_torrent(source).await.unwrap();
let export = engine.export().await.unwrap();

assert_eq!(torrent.info_hash().to_hex(), BIG_BUCK_BUNNY_INFO_HASH);
assert_eq!(export.torrents.len(), 1);
}

#[tokio::test]
async fn engine_when_torrent_source_type_does_not_match_then_returns_typed_error() {
let engine = Engine::builder().autostart(false).build();
let source = TorrentSource::remote_torrent_url("magnet:?xt=urn:btih:not-a-url");

let error = engine.add_torrent(source).await.unwrap_err();

assert!(matches!(
error,
EngineError::InvalidTorrentSource {
source_type: "remote URL",
..
}
));
}
}
Loading
Loading