From 2778d46bb3679b295d194fe12ca031177da79013 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Tue, 5 Mar 2024 09:18:22 +0000 Subject: [PATCH] End-to-end unit test (#90) * First implementation of create_torrent_file * Test harness for e2e preparing * Saving * Continuing test harness * Continuing test harness * Continuing test harness * All servers are running * Full e2e harness done * Test e2e harness working fine * Remove lints * injecting faults * The e2e test runs continuously * e2e test * Add a test for create_torrent * Nothing * Nothing, just tweaking the numberes * Update tokio, remove custom tempfile shim --- Cargo.lock | 5 +- crates/librqbit/Cargo.toml | 1 + crates/librqbit/src/chunk_tracker.rs | 2 +- crates/librqbit/src/create_torrent_file.rs | 238 ++++++++++++++++ crates/librqbit/src/lib.rs | 5 + crates/librqbit/src/peer_connection.rs | 21 +- crates/librqbit/src/session.rs | 4 + crates/librqbit/src/tests/e2e.rs | 271 +++++++++++++++++++ crates/librqbit/src/tests/mod.rs | 2 + crates/librqbit/src/tests/test_util.rs | 66 +++++ crates/librqbit_core/src/lengths.rs | 18 +- crates/librqbit_core/src/torrent_metainfo.rs | 17 +- desktop/src-tauri/Cargo.lock | 4 +- 13 files changed, 634 insertions(+), 20 deletions(-) create mode 100644 crates/librqbit/src/create_torrent_file.rs create mode 100644 crates/librqbit/src/tests/e2e.rs create mode 100644 crates/librqbit/src/tests/mod.rs create mode 100644 crates/librqbit/src/tests/test_util.rs diff --git a/Cargo.lock b/Cargo.lock index b49f094..33a5f94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,6 +1292,7 @@ dependencies = [ "serde_with", "sha1", "size_format", + "tempfile", "tokio", "tokio-stream", "tokio-test", @@ -2483,9 +2484,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", diff --git a/crates/librqbit/Cargo.toml b/crates/librqbit/Cargo.toml index 9ba6353..5f43844 100644 --- a/crates/librqbit/Cargo.toml +++ b/crates/librqbit/Cargo.toml @@ -75,3 +75,4 @@ async-stream = "0.3.5" futures = {version = "0.3"} tracing-subscriber = "0.3" tokio-test = "0.4" +tempfile = "3" diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index 8fc8c97..dca5b47 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -41,7 +41,7 @@ fn compute_chunk_status(lengths: &Lengths, needed_pieces: &BF) -> BF { .unwrap() .iter_zeros() { - let offset = piece_index * lengths.default_chunks_per_piece() as usize; + let offset = piece_index * lengths.default_max_chunks_per_piece() as usize; let chunks_per_piece = lengths .chunks_per_piece(lengths.validate_piece_index(piece_index as u32).unwrap()) as usize; diff --git a/crates/librqbit/src/create_torrent_file.rs b/crates/librqbit/src/create_torrent_file.rs new file mode 100644 index 0000000..1629bcf --- /dev/null +++ b/crates/librqbit/src/create_torrent_file.rs @@ -0,0 +1,238 @@ +use std::borrow::Cow; +use std::io::{BufWriter, Read}; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use anyhow::Context; +use bencode::bencode_serialize_to_writer; +use buffers::ByteString; +use librqbit_core::torrent_metainfo::{TorrentMetaV1File, TorrentMetaV1Info, TorrentMetaV1Owned}; +use librqbit_core::Id20; +use sha1w::{ISha1, Sha1}; + +use crate::spawn_utils::BlockingSpawner; + +#[derive(Debug, Clone, Default)] +pub struct CreateTorrentOptions<'a> { + pub name: Option<&'a str>, + pub piece_length: Option, +} + +fn walk_dir_find_paths(dir: &Path, out: &mut Vec>) -> anyhow::Result<()> { + let mut stack = vec![Cow::Borrowed(dir)]; + while let Some(dir) = stack.pop() { + let rd = std::fs::read_dir(&dir).with_context(|| format!("error reading {:?}", dir))?; + for element in rd { + let element = + element.with_context(|| format!("error reading DirEntry from {:?}", dir))?; + let ft = element.file_type().with_context(|| { + format!( + "error determining filetype of DirEntry {:?} while reading {:?}", + element.file_name(), + dir + ) + })?; + + let full_path = Cow::Owned(dir.join(element.file_name())); + if ft.is_dir() { + stack.push(full_path); + } else { + out.push(full_path); + } + } + } + Ok(()) +} + +fn compute_info_hash(t: &TorrentMetaV1Info) -> anyhow::Result { + struct W { + hash: sha1w::Sha1, + } + impl std::io::Write for W { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.hash.update(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + let mut writer = BufWriter::new(W { hash: Sha1::new() }); + bencode_serialize_to_writer(t, &mut writer)?; + let hash = writer + .into_inner() + .map_err(|_| anyhow::anyhow!("into_inner errored"))? + .hash; + Ok(Id20::new(hash.finish())) +} + +fn choose_piece_length(_input_files: &[Cow<'_, Path>]) -> u32 { + // TODO: make this smarter or smth + 2 * 1024 * 1024 +} + +async fn create_torrent_raw<'a>( + path: &'a Path, + options: CreateTorrentOptions<'a>, +) -> anyhow::Result> { + path.try_exists() + .with_context(|| format!("path {:?} doesn't exist", path))?; + let basename = path + .file_name() + .ok_or_else(|| anyhow::anyhow!("cannot determine basename of {:?}", path))?; + let is_dir = path.is_dir(); + let single_file_mode = !is_dir; + let name: ByteString = match options.name { + Some(name) => name.as_bytes().into(), + None => basename.as_bytes().into(), + }; + + let mut input_files: Vec> = Default::default(); + if is_dir { + walk_dir_find_paths(path, &mut input_files) + .with_context(|| format!("error walking {:?}", path))?; + } else { + input_files.push(Cow::Borrowed(path)); + } + + let piece_length = options + .piece_length + .unwrap_or_else(|| choose_piece_length(&input_files)); + + // Calculate hashes etc. + const READ_SIZE: u32 = 8192; // todo: twea + let mut read_buf = vec![0; READ_SIZE as usize]; + + let mut length = 0; + let mut remaining_piece_length = piece_length; + let mut piece_checksum = sha1w::Sha1::new(); + let mut piece_hashes = Vec::::new(); + let mut output_files: Vec> = Vec::new(); + + let spawner = BlockingSpawner::default(); + + 'outer: for file in input_files { + let filename = &*file; + length = 0; + let mut fd = std::io::BufReader::new( + std::fs::File::open(&file).with_context(|| format!("error opening {:?}", filename))?, + ); + + loop { + let max_bytes_to_read = remaining_piece_length.min(READ_SIZE) as usize; + let size = spawner + .spawn_block_in_place(|| fd.read(&mut read_buf[..max_bytes_to_read])) + .with_context(|| format!("error reading {:?}", filename))?; + + // EOF: swap file + if size == 0 { + let filename = filename + .strip_prefix(path) + .context("internal error, can't strip prefix")?; + let path = filename + .components() + .map(|c| c.as_os_str().as_bytes().into()) + .collect(); + output_files.push(TorrentMetaV1File { length, path }); + continue 'outer; + } + + length += size as u64; + piece_checksum.update(&read_buf[..size]); + + remaining_piece_length -= size as u32; + if remaining_piece_length == 0 { + remaining_piece_length = piece_length; + piece_hashes.extend_from_slice(&piece_checksum.finish()); + piece_checksum = sha1w::Sha1::new(); + } + } + } + + if remaining_piece_length > 0 && length > 0 { + piece_hashes.extend_from_slice(&piece_checksum.finish()); + } + Ok(TorrentMetaV1Info { + name: Some(name), + pieces: piece_hashes.into(), + piece_length, + length: if single_file_mode { Some(length) } else { None }, + md5sum: None, + files: if single_file_mode { + None + } else { + Some(output_files) + }, + }) +} + +#[derive(Debug)] +pub struct CreateTorrentResult { + meta: TorrentMetaV1Owned, +} + +impl CreateTorrentResult { + pub fn as_info(&self) -> &TorrentMetaV1Owned { + &self.meta + } + + pub fn info_hash(&self) -> Id20 { + self.meta.info_hash + } + + pub fn as_bytes(&self) -> anyhow::Result> { + let mut b = Vec::new(); + bencode_serialize_to_writer(&self.meta, &mut b).context("error serializing torrent")?; + Ok(b) + } +} + +pub async fn create_torrent<'a>( + path: &'a Path, + options: CreateTorrentOptions<'a>, +) -> anyhow::Result { + let info = create_torrent_raw(path, options).await?; + let info_hash = compute_info_hash(&info).context("error computing info hash")?; + Ok(CreateTorrentResult { + meta: TorrentMetaV1Owned { + announce: b""[..].into(), + announce_list: Vec::new(), + info, + comment: None, + created_by: None, + encoding: Some(b"utf-8"[..].into()), + publisher: None, + publisher_url: None, + creation_date: None, + info_hash, + }, + }) +} + +#[cfg(test)] +mod tests { + use buffers::ByteBuf; + use librqbit_core::torrent_metainfo::torrent_from_bytes; + + use crate::create_torrent; + + #[tokio::test] + async fn test_create_torrent() { + use crate::tests::test_util; + + let dir = test_util::create_default_random_dir_with_torrents( + 3, + 1000 * 1000, + Some("rqbit_test_create_torrent"), + ); + let torrent = create_torrent(dir.path(), Default::default()) + .await + .unwrap(); + + let bytes = torrent.as_bytes().unwrap(); + + let deserialized = torrent_from_bytes::(&bytes).unwrap(); + assert_eq!(torrent.info_hash(), deserialized.info_hash); + } +} diff --git a/crates/librqbit/src/lib.rs b/crates/librqbit/src/lib.rs index 4c59346..eadf722 100644 --- a/crates/librqbit/src/lib.rs +++ b/crates/librqbit/src/lib.rs @@ -25,6 +25,7 @@ pub mod api; mod api_error; mod chunk_tracker; +mod create_torrent_file; mod dht_utils; mod file_ops; pub mod http_api; @@ -40,6 +41,7 @@ mod type_aliases; pub use api::Api; pub use api_error::ApiError; +pub use create_torrent_file::{create_torrent, CreateTorrentOptions}; pub use dht; pub use peer_connection::PeerConnectionOptions; pub use session::{ @@ -55,6 +57,9 @@ pub use librqbit_core::magnet::*; pub use librqbit_core::peer_id::*; pub use librqbit_core::torrent_metainfo::*; +#[cfg(test)] +mod tests; + /// The cargo version of librqbit. pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") diff --git a/crates/librqbit/src/peer_connection.rs b/crates/librqbit/src/peer_connection.rs index ad3a521..1ac208d 100644 --- a/crates/librqbit/src/peer_connection.rs +++ b/crates/librqbit/src/peer_connection.rs @@ -181,7 +181,10 @@ impl PeerConnection { .await .context("error reading handshake")?; let h_supports_extended = h.supports_extended(); - trace!("connected: id={:?}", try_decode_peer_id(Id20::new(h.peer_id))); + trace!( + "connected: id={:?}", + try_decode_peer_id(Id20::new(h.peer_id)) + ); if h.info_hash != self.info_hash.0 { anyhow::bail!("info hash does not match"); } @@ -269,6 +272,22 @@ impl PeerConnection { .and_then(|e| e.ut_metadata()) })?, WriterRequest::ReadChunkRequest(chunk) => { + #[cfg(test)] + { + // This is poor-mans fault injection for running e2e tests. + use crate::tests::test_util::TestPeerMetadata; + let tpm = TestPeerMetadata::from_peer_id(self.peer_id); + use rand::Rng; + if rand::thread_rng().gen_bool(tpm.disconnect_probability()) { + bail!("disconnecting, to simulate failure in tests"); + } + + let sleep_ms = (rand::thread_rng().gen::() + * (tpm.max_random_sleep_ms as f64)) + as u64; + tokio::time::sleep(Duration::from_millis(sleep_ms)).await; + } + // this whole section is an optimization write_buf.resize(PIECE_MESSAGE_DEFAULT_LEN, 0); let preamble_len = serialize_piece_preamble(chunk, &mut write_buf); diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index c3bb138..131756d 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -1136,6 +1136,10 @@ impl Session { handle.start(peer_rx, false, self.cancellation_token.child_token())?; Ok(()) } + + pub fn tcp_listen_port(&self) -> Option { + self.tcp_listen_port + } } // Ad adapter for converting stats into the format that tracker_comms accepts. diff --git a/crates/librqbit/src/tests/e2e.rs b/crates/librqbit/src/tests/e2e.rs new file mode 100644 index 0000000..b6b71ab --- /dev/null +++ b/crates/librqbit/src/tests/e2e.rs @@ -0,0 +1,271 @@ +use std::{ + borrow::Cow, + net::{Ipv4Addr, SocketAddr}, + time::Duration, +}; + +use anyhow::bail; +use futures::{stream::FuturesUnordered, StreamExt}; +use rand::Rng; +use tokio::{ + spawn, + time::{interval, timeout}, +}; +use tracing::{error_span, info, Instrument}; + +use crate::{ + create_torrent, + tests::test_util::{create_default_random_dir_with_torrents, TestPeerMetadata}, + AddTorrentOptions, AddTorrentResponse, Session, SessionOptions, +}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 64)] +async fn test_e2e() { + let _ = tracing_subscriber::fmt::try_init(); + + // 1. Create a torrent + // Ideally (for a more complicated test) with N files, and at least N pieces that span 2 files. + + let piece_length: u32 = 16384 * 2; // TODO: figure out if this should be multiple of chunk size or not + let file_length: usize = 1000 * 1000; + let num_files: usize = 64; + + let tempdir = + create_default_random_dir_with_torrents(num_files, file_length, Some("rqbit_e2e")); + let torrent_file = create_torrent( + dbg!(tempdir.path()), + crate::CreateTorrentOptions { + piece_length: Some(piece_length), + ..Default::default() + }, + ) + .await + .unwrap(); + + let num_servers = 128; + + let torrent_file_bytes = torrent_file.as_bytes().unwrap(); + let mut futs = FuturesUnordered::new(); + + // 2. Start N servers that are serving that torrent, and return their IP:port combos. + // Disable DHT on each. + for i in 0u8..num_servers { + let torrent_file_bytes = torrent_file_bytes.clone(); + let (tx, rx) = tokio::sync::oneshot::channel(); + let tempdir = tempdir.path().to_owned(); + spawn( + async move { + let peer_id = TestPeerMetadata { + server_id: i, + max_random_sleep_ms: rand::thread_rng().gen_range(0u8..128), + } + .as_peer_id(); + let session = crate::Session::new_with_opts( + std::env::temp_dir().join("does_not_exist"), + SessionOptions { + disable_dht: true, + disable_dht_persistence: true, + dht_config: None, + persistence: false, + persistence_filename: None, + peer_id: Some(peer_id), + peer_opts: None, + listen_port_range: Some(15100..17000), + enable_upnp_port_forwarding: false, + }, + ) + .await + .unwrap(); + + info!("started session"); + + let handle = session + .add_torrent( + crate::AddTorrent::TorrentFileBytes(Cow::Owned(torrent_file_bytes)), + Some(AddTorrentOptions { + overwrite: true, + output_folder: Some(tempdir.to_str().unwrap().to_owned()), + ..Default::default() + }), + ) + .await + .unwrap(); + let h = handle.into_handle().unwrap(); + let mut interval = interval(Duration::from_millis(100)); + + info!("added torrent"); + loop { + interval.tick().await; + let is_live = h + .with_state(|s| match s { + crate::ManagedTorrentState::Initializing(_) => Ok(false), + crate::ManagedTorrentState::Live(l) => { + if !l.is_finished() { + bail!("torrent went live, but expected it to be finished"); + } + Ok(true) + } + _ => bail!("broken state"), + }) + .unwrap(); + if is_live { + break; + } + } + info!("torrent is live"); + tx.send(SocketAddr::new( + std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + session.tcp_listen_port().unwrap(), + )) + } + .instrument(error_span!("server", server = i)), + ); + futs.push(timeout(Duration::from_secs(10), rx)); + } + + let mut peers = Vec::new(); + while let Some(addr) = futs.next().await { + peers.push(addr.unwrap().unwrap()); + } + + info!("started all servers, starting client"); + + let client_iters = std::env::var("E2E_CLIENT_ITERS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1usize); + + // 3. Start a client with the initial peers, and download the file. + for _ in 0..client_iters { + let outdir = tempfile::TempDir::with_prefix("rqbit_e2e_client").unwrap(); + let session = Session::new_with_opts( + outdir.path().to_owned(), + SessionOptions { + disable_dht: true, + disable_dht_persistence: true, + dht_config: None, + persistence: false, + persistence_filename: None, + listen_port_range: None, + enable_upnp_port_forwarding: false, + ..Default::default() + }, + ) + .await + .unwrap(); + + info!("started client session"); + + let (id, handle) = { + let r = session + .add_torrent( + crate::AddTorrent::TorrentFileBytes(Cow::Owned(torrent_file_bytes.clone())), + Some(AddTorrentOptions { + initial_peers: Some(peers.clone()), + overwrite: false, + ..Default::default() + }), + ) + .await + .unwrap(); + + match r { + AddTorrentResponse::AlreadyManaged(_, _) => todo!(), + AddTorrentResponse::ListOnly(_) => todo!(), + AddTorrentResponse::Added(id, h) => (id, h), + } + }; + + info!("added handle"); + + { + let stats_printer = { + let handle = handle.clone(); + async move { + let mut interval = interval(Duration::from_millis(100)); + + loop { + interval.tick().await; + let stats = handle.stats(); + let live = match &stats.live { + Some(live) => live, + None => continue, + }; + let pstats = &live.snapshot.peer_stats; + + info!( + progress_percent = + format!("{}", stats.progress_percent_human_readable()), + peers = format!("{:?}", pstats), + ); + } + } + } + .instrument(error_span!("stats_printer")); + + let timeout = timeout(Duration::from_secs(60), handle.wait_until_completed()); + + tokio::pin!(stats_printer); + tokio::pin!(timeout); + + let mut stats_finished = false; + loop { + tokio::select! { + r = &mut timeout => { + r.unwrap().unwrap(); + break; + } + _ = &mut stats_printer, if !stats_finished => { + stats_finished = true; + } + } + } + } + + info!("handle is completed"); + session.delete(id, false).unwrap(); + + info!("deleted handle"); + + // 4. After downloading, recheck its integrity. + let handle = session + .add_torrent( + crate::AddTorrent::TorrentFileBytes(Cow::Owned(torrent_file_bytes.clone())), + Some(AddTorrentOptions { + paused: true, + overwrite: true, + ..Default::default() + }), + ) + .await + .unwrap() + .into_handle() + .unwrap(); + + info!("re-added handle"); + + timeout(Duration::from_secs(10), async { + let mut interval = interval(Duration::from_millis(100)); + loop { + interval.tick().await; + let b = handle + .with_state(|s| match s { + crate::ManagedTorrentState::Initializing(_) => Ok(false), + crate::ManagedTorrentState::Paused(p) => { + assert_eq!(p.needed_bytes, 0); + Ok(true) + } + _ => bail!("bugged state"), + }) + .unwrap(); + if b { + break; + } + } + }) + .await + .unwrap(); + + info!("all good"); + } +} diff --git a/crates/librqbit/src/tests/mod.rs b/crates/librqbit/src/tests/mod.rs new file mode 100644 index 0000000..8a9bd01 --- /dev/null +++ b/crates/librqbit/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod e2e; +pub mod test_util; diff --git a/crates/librqbit/src/tests/test_util.rs b/crates/librqbit/src/tests/test_util.rs new file mode 100644 index 0000000..e72780a --- /dev/null +++ b/crates/librqbit/src/tests/test_util.rs @@ -0,0 +1,66 @@ +use std::{io::Write, path::Path}; + +use librqbit_core::Id20; +use rand::RngCore; +use tempfile::TempDir; + +pub fn create_new_file_with_random_content(path: &Path, mut size: usize) { + let mut file = std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(path) + .unwrap(); + + eprintln!("creating temp file {:?}", path); + + const BUF_SIZE: usize = 8192 * 16; + let mut rng = rand::rngs::OsRng; + let mut write_buf = [0; BUF_SIZE]; + while size > 0 { + rng.fill_bytes(&mut write_buf[..]); + let written = file.write(&write_buf[..size.min(BUF_SIZE)]).unwrap(); + size -= written; + } +} + +pub fn create_default_random_dir_with_torrents( + num_files: usize, + file_size: usize, + tempdir_prefix: Option<&str>, +) -> TempDir { + let dir = TempDir::with_prefix(tempdir_prefix.unwrap_or("rqbit_test")).unwrap(); + dbg!(dir.path()); + for f in 0..num_files { + create_new_file_with_random_content(&dir.path().join(&format!("{f}.data")), file_size); + } + dir +} + +#[derive(Debug)] +pub struct TestPeerMetadata { + pub server_id: u8, + pub max_random_sleep_ms: u8, +} + +impl TestPeerMetadata { + pub fn as_peer_id(&self) -> Id20 { + let mut peer_id = Id20::default(); + peer_id.0[0] = self.server_id; + peer_id.0[1] = self.max_random_sleep_ms; + peer_id + } + + pub fn from_peer_id(peer_id: Id20) -> Self { + Self { + server_id: peer_id.0[0], + max_random_sleep_ms: peer_id.0[1], + } + } + + pub fn disconnect_probability(&self) -> f64 { + if self.server_id % 2 == 0 { + return 0.05f64; + } + 0f64 + } +} diff --git a/crates/librqbit_core/src/lengths.rs b/crates/librqbit_core/src/lengths.rs index 334068b..63a083c 100644 --- a/crates/librqbit_core/src/lengths.rs +++ b/crates/librqbit_core/src/lengths.rs @@ -38,7 +38,7 @@ pub struct Lengths { piece_length: u32, last_piece_id: u32, last_piece_length: u32, - chunks_per_piece: u32, + max_chunks_per_piece: u32, } #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -84,7 +84,7 @@ impl Lengths { } if chunk_length > piece_length { anyhow::bail!( - "chunk length {} should be smaller than or equal to piece length {}", + "chunk length {} should be >= piece length {}", chunk_length, piece_length ); @@ -97,7 +97,7 @@ impl Lengths { chunk_length, piece_length, total_length, - chunks_per_piece: ceil_div_u64(piece_length as u64, chunk_length as u64) as u32, + max_chunks_per_piece: ceil_div_u64(piece_length as u64, chunk_length as u64) as u32, last_piece_id: total_pieces - 1, last_piece_length: last_element_size_u64(total_length, piece_length as u64) as u32, }) @@ -123,8 +123,8 @@ impl Lengths { pub const fn default_chunk_length(&self) -> u32 { self.chunk_length } - pub const fn default_chunks_per_piece(&self) -> u32 { - self.chunks_per_piece + pub const fn default_max_chunks_per_piece(&self) -> u32 { + self.max_chunks_per_piece } pub const fn total_chunks(&self) -> u32 { ceil_div_u64(self.total_length, self.chunk_length as u64) as u32 @@ -161,7 +161,7 @@ impl Lengths { pub fn iter_chunk_infos(&self, index: ValidPieceIndex) -> impl Iterator { let mut remaining = self.piece_length(index); let chunk_size = self.chunk_length; - let absolute_offset = index.0 * self.chunks_per_piece; + let absolute_offset = index.0 * self.max_chunks_per_piece; (0u32..).scan(0, move |offset, idx| { if remaining == 0 { return None; @@ -195,7 +195,7 @@ impl Lengths { if expected_chunk_size != chunk_size { return None; } - let absolute_index = self.chunks_per_piece * piece_index.get() + index; + let absolute_index = self.max_chunks_per_piece * piece_index.get() + index; Some(ChunkInfo { piece_index, chunk_index: index, @@ -214,7 +214,7 @@ impl Lengths { self.chunk_info_from_received_data(self.validate_piece_index(index)?, begin, block_len) } pub const fn chunk_range(&self, index: ValidPieceIndex) -> std::ops::Range { - let start = index.0 * self.chunks_per_piece; + let start = index.0 * self.max_chunks_per_piece; let end = start + self.chunks_per_piece(index); start as usize..end as usize } @@ -222,7 +222,7 @@ impl Lengths { if index.0 == self.last_piece_id { return (self.last_piece_length + self.chunk_length - 1) / self.chunk_length; } - self.chunks_per_piece + self.max_chunks_per_piece } pub const fn chunk_offset_in_piece( &self, diff --git a/crates/librqbit_core/src/torrent_metainfo.rs b/crates/librqbit_core/src/torrent_metainfo.rs index d060606..3734e64 100644 --- a/crates/librqbit_core/src/torrent_metainfo.rs +++ b/crates/librqbit_core/src/torrent_metainfo.rs @@ -27,20 +27,27 @@ pub fn torrent_from_bytes<'de, ByteBuf: Deserialize<'de>>( } /// A parsed .torrent file. -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct TorrentMetaV1 { pub announce: BufType, - #[serde(rename = "announce-list", default = "Vec::new")] + #[serde( + rename = "announce-list", + default = "Vec::new", + skip_serializing_if = "Vec::is_empty" + )] pub announce_list: Vec>, pub info: TorrentMetaV1Info, + #[serde(skip_serializing_if = "Option::is_none")] pub comment: Option, - #[serde(rename = "created by")] + #[serde(rename = "created by", skip_serializing_if = "Option::is_none")] pub created_by: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub encoding: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub publisher: Option, - #[serde(rename = "publisher-url")] + #[serde(rename = "publisher-url", skip_serializing_if = "Option::is_none")] pub publisher_url: Option, - #[serde(rename = "creation date")] + #[serde(rename = "creation date", skip_serializing_if = "Option::is_none")] pub creation_date: Option, #[serde(skip)] diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index f49fc67..0e2a4d8 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -3958,9 +3958,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes",