├── .envrc ├── crates ├── vcz-macros │ ├── src │ │ ├── utils.rs │ │ └── lib.rs │ ├── tests │ │ └── tests.rs │ └── Cargo.toml ├── vcz-ui │ ├── src │ │ ├── widgets │ │ │ ├── mod.rs │ │ │ └── network_chart.rs │ │ ├── main.rs │ │ ├── action.rs │ │ ├── pages │ │ │ └── mod.rs │ │ ├── error.rs │ │ ├── palette.rs │ │ ├── tui.rs │ │ ├── lib.rs │ │ └── app.rs │ └── Cargo.toml ├── vcz-lib │ ├── src │ │ ├── extensions │ │ │ ├── mod.rs │ │ │ ├── extended │ │ │ │ ├── trait.rs │ │ │ │ └── codec.rs │ │ │ ├── core │ │ │ │ └── mod.rs │ │ │ ├── metadata │ │ │ │ └── codec.rs │ │ │ └── holepunch │ │ │ │ ├── codec.rs │ │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── tracker │ │ │ ├── event.rs │ │ │ ├── announce.rs │ │ │ ├── action.rs │ │ │ ├── connect.rs │ │ │ └── mod.rs │ │ ├── utils.rs │ │ ├── bitfield.rs │ │ ├── error.rs │ │ ├── magnet.rs │ │ ├── counter.rs │ │ ├── torrent │ │ │ ├── from_meta_info.rs │ │ │ └── from_magnet.rs │ │ ├── config.rs │ │ └── daemon_wire │ │ │ └── mod.rs │ └── Cargo.toml ├── utp │ ├── Cargo.toml │ └── src │ │ ├── listener.rs │ │ ├── packet.rs │ │ └── lib.rs ├── vcz-test │ ├── Cargo.toml │ └── tests │ │ ├── unchoke-algorithm.rs │ │ ├── common │ │ ├── peer.rs │ │ ├── mod.rs │ │ └── tracker.rs │ │ └── requests.rs ├── vcz-daemon │ ├── Cargo.toml │ └── src │ │ └── main.rs └── vcz │ ├── Cargo.toml │ └── src │ └── main.rs ├── tape.gif ├── test-files ├── pieces.iso ├── book.torrent ├── debian.torrent └── complete │ └── t.torrent ├── .gitignore ├── rustfmt.toml ├── spellcheck.toml ├── rust-toolchain.toml ├── LICENSE ├── tape.tape ├── flake.nix ├── .github └── workflows │ ├── release.yml │ └── build.yml ├── Cargo.toml └── README.md /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /crates/vcz-macros/src/utils.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tape.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/HEAD/tape.gif -------------------------------------------------------------------------------- /crates/vcz-ui/src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod network_chart; 2 | pub mod vim_input; 3 | -------------------------------------------------------------------------------- /test-files/pieces.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/HEAD/test-files/pieces.iso -------------------------------------------------------------------------------- /test-files/book.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/HEAD/test-files/book.torrent -------------------------------------------------------------------------------- /test-files/debian.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/HEAD/test-files/debian.torrent -------------------------------------------------------------------------------- /test-files/complete/t.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/HEAD/test-files/complete/t.torrent -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | log.txt 4 | perf.data 5 | flamegraph.svg 6 | **.DS_Store 7 | /bin 8 | rustc-ice* 9 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | comment_width = 80 3 | format_code_in_doc_comments = true 4 | imports_granularity = "Crate" 5 | wrap_comments = true 6 | use_small_heuristics = "Max" 7 | format_macro_matchers = true 8 | format_strings = true 9 | -------------------------------------------------------------------------------- /spellcheck.toml: -------------------------------------------------------------------------------- 1 | dev_comments = false 2 | skip_readme = false 3 | 4 | [Hunspell] 5 | lang = "en_US" 6 | search_dirs = ["."] 7 | skip_os_lookups = true 8 | use_builtin = true 9 | 10 | [Hunspell.quirks] 11 | allow_concatenation = true 12 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Extensions (protocols) that act on Peers, including the core protocol. 2 | 3 | pub mod core; 4 | pub mod extended; 5 | pub mod holepunch; 6 | pub mod metadata; 7 | 8 | pub use core::*; 9 | pub use extended::*; 10 | pub use holepunch::*; 11 | pub use metadata::*; 12 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-12-05" 3 | components = [ "rustfmt", "clippy", "rust-docs" ] 4 | targets = [ "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "x86_64-pc-windows-gnu", "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-apple-darwin" ] 5 | profile = "default" 6 | -------------------------------------------------------------------------------- /crates/utp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utp" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | tokio = { workspace = true } 8 | hashbrown = { workspace = true } 9 | tokio-util = { workspace = true } 10 | rand = { workspace = true } 11 | int-enum = { workspace = true } 12 | 13 | # [dev-dependencies] 14 | # futures = { workspace = true } 15 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(macro_metavar_expr)] 2 | #![feature(ip_as_octets)] 3 | #![feature(trait_alias)] 4 | 5 | pub mod bitfield; 6 | pub mod config; 7 | pub mod counter; 8 | pub mod daemon; 9 | pub mod daemon_wire; 10 | pub mod disk; 11 | pub mod error; 12 | pub mod extensions; 13 | pub mod magnet; 14 | pub mod metainfo; 15 | pub mod peer; 16 | pub mod torrent; 17 | pub mod tracker; 18 | pub mod utils; 19 | -------------------------------------------------------------------------------- /crates/vcz-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz-test" 3 | version = "0.0.1" 4 | authors = ["Gabriel Lombardo "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | rand = { workspace = true } 9 | bendy = { workspace = true } 10 | bitvec = { workspace = true } 11 | futures = { workspace = true } 12 | hashbrown = { workspace = true } 13 | tokio = { workspace = true } 14 | tokio-util = { workspace = true } 15 | vcz-lib = { workspace = true, features = ["debug"] } 16 | 17 | # [lints] 18 | # workspace = true 19 | -------------------------------------------------------------------------------- /crates/vcz-macros/tests/tests.rs: -------------------------------------------------------------------------------- 1 | // use vcz_macros::*; 2 | 3 | #[test] 4 | fn derive_ext() { 5 | // struct Codex; 6 | // struct Msg; 7 | // 8 | // trait Codec { 9 | // type Msg; 10 | // } 11 | // 12 | // enum A { 13 | // A, 14 | // B, 15 | // } 16 | // 17 | // impl Codec for A { 18 | // type Msg = u32; 19 | // } 20 | // 21 | // declare_message!(A); 22 | // 23 | // #[derive(Extension)] 24 | // #[extension(id = 0, codec = Codec, msg = Msg)] 25 | // struct MyExt; 26 | } 27 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tokio::sync::mpsc; 4 | use vcz_lib::config::Config; 5 | use vcz_ui::{app::App, error::Error}; 6 | 7 | #[tokio::main(flavor = "current_thread")] 8 | async fn main() -> Result<(), Error> { 9 | let config = Arc::new(Config::load()?); 10 | 11 | let (fr_tx, fr_rx) = mpsc::unbounded_channel(); 12 | 13 | // Start and run the terminal UI 14 | let mut app = App::new(config, fr_tx.clone()); 15 | 16 | // UI is detached from the Daemon 17 | app.is_detached = true; 18 | 19 | app.run(fr_rx).await?; 20 | 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /crates/vcz-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz-macros" 3 | version = "0.0.1" 4 | description = "Procedural macros of Vincenzo" 5 | keywords = ["distributed", "bittorrent", "torrent", "p2p", "networking"] 6 | categories = ["network-programming"] 7 | repository = "https://github.com/gabrieldemian/vincenzo" 8 | authors = ["Gabriel Lombardo "] 9 | edition = "2024" 10 | readme = "README.md" 11 | license = "MIT" 12 | exclude = ["tests/*", "*.log"] 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [[test]] 18 | name = "tests" 19 | 20 | [dependencies] 21 | quote = "1.0.41" 22 | syn = "2.0.106" 23 | darling = "0.21.3" 24 | -------------------------------------------------------------------------------- /crates/vcz-ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz-ui" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | crossterm = { version = "0.28.1", features = ["event-stream"] } 10 | clap = { workspace = true } 11 | futures = { workspace = true } 12 | hashbrown = { workspace = true } 13 | magnet-url = { workspace = true } 14 | rand = { workspace = true } 15 | ratatui = { workspace = true } 16 | serde = { workspace = true } 17 | thiserror = { workspace = true } 18 | tokio = { workspace = true } 19 | tokio-util = { workspace = true } 20 | tracing = { workspace = true } 21 | vcz-lib = { workspace = true } 22 | unicode-width = "=0.2.0" 23 | 24 | [features] 25 | debug = [] 26 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/action.rs: -------------------------------------------------------------------------------- 1 | use vcz_lib::torrent::{InfoHash, TorrentState}; 2 | 3 | use crate::Input; 4 | 5 | /// A new component to be rendered on the UI. 6 | /// Used in conjunction with [`Action`] 7 | #[derive(Clone, Copy)] 8 | pub enum Page { 9 | // first page to be rendered 10 | TorrentList, 11 | // Details, 12 | } 13 | 14 | #[derive(Clone)] 15 | pub enum Action { 16 | Tick, 17 | Render, 18 | Quit, 19 | Error, 20 | None, 21 | 22 | TerminalEvent(crossterm::event::Event), 23 | 24 | /// First the page will process TerminalEvent and transform it into Input. 25 | Input(Input), 26 | 27 | NewTorrent(magnet_url::Magnet), 28 | TogglePause(InfoHash), 29 | DeleteTorrent(InfoHash), 30 | TorrentState(TorrentState), 31 | TorrentStates(Vec), 32 | } 33 | -------------------------------------------------------------------------------- /crates/vcz-daemon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz-daemon" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "vczd" 8 | path = "src/main.rs" 9 | 10 | [dependencies] 11 | bendy = { workspace = true } 12 | bytes = { workspace = true } 13 | clap = { workspace = true } 14 | futures = { workspace = true } 15 | hashbrown = { workspace = true } 16 | hex = { workspace = true } 17 | magnet-url = { workspace = true } 18 | rand = { workspace = true } 19 | serde = { workspace = true } 20 | sha1 = { workspace = true } 21 | thiserror = { workspace = true } 22 | tokio = { workspace = true } 23 | tokio-util = { workspace = true } 24 | toml = { workspace = true } 25 | tracing = { workspace = true } 26 | tracing-subscriber = { workspace = true } 27 | urlencoding = { workspace = true } 28 | vcz-lib = { workspace = true } 29 | 30 | [features] 31 | debug = [] 32 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/extensions/extended/trait.rs: -------------------------------------------------------------------------------- 1 | //! Types for extensions of the peer protocol. 2 | 3 | use crate::extensions::ExtendedMessage; 4 | use std::future::Future; 5 | 6 | /// Messages of the extension, usually an enum. 7 | /// The ID const is the local peer's. The IDs of the remote peers are shared on 8 | /// the [`Extension`] struct, under the "M" dict and they are different from 9 | /// client to client. 10 | pub trait ExtMsg: TryFrom { 11 | const ID: u8; 12 | } 13 | 14 | /// Message handler trait for an extension. 15 | /// 16 | /// Extensions will implement this trait for `Peer`. 17 | pub trait ExtMsgHandler 18 | where 19 | Msg: ExtMsg, 20 | Self::Error: From<>::Error>, 21 | { 22 | type Error: std::error::Error; 23 | 24 | fn handle_msg( 25 | &mut self, 26 | msg: Msg, 27 | ) -> impl Future>; 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Gabriel Lombardo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | //! A page fills the entire screen and can have 0..n components. 2 | //! 3 | //! A page draws on the screen by receiving an Event and transforming it into an 4 | //! Action, and then it uses this Action on it's draw function to do whatever it 5 | //! wants. 6 | 7 | use ratatui::Frame; 8 | pub mod torrent_list; 9 | 10 | use crate::{action::Action, tui::Event}; 11 | 12 | pub trait Page { 13 | /// Draw on the screen, can also call draw on it's components. 14 | fn draw(&mut self, f: &mut Frame); 15 | 16 | fn handle_event(&mut self, event: Event) -> Action; 17 | 18 | /// Handle an action, for example, key presses, change to another page, etc. 19 | fn handle_action(&mut self, action: Action); 20 | 21 | /// get an app event and transform into a page action 22 | fn get_action(&self, event: Event) -> Action; 23 | 24 | /// Focus on the next component, if available. 25 | fn focus_next(&mut self); 26 | 27 | /// Focus on the previous component, if available. 28 | fn focus_prev(&mut self); 29 | } 30 | -------------------------------------------------------------------------------- /crates/vcz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | [[bin]] 7 | name = "vcz" 8 | path = "src/main.rs" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | bendy = { workspace = true } 14 | bytes = { workspace = true } 15 | clap = { workspace = true } 16 | futures = { workspace = true } 17 | hashbrown = { workspace = true } 18 | hex = { workspace = true } 19 | magnet-url = { workspace = true } 20 | rand = { workspace = true } 21 | ratatui = { workspace = true } 22 | serde = { workspace = true } 23 | sha1 = { workspace = true } 24 | thiserror = { workspace = true } 25 | tokio = { workspace = true } 26 | tokio-util = { workspace = true } 27 | toml = { workspace = true } 28 | tracing = { workspace = true } 29 | tracing-subscriber = { workspace = true } 30 | urlencoding = { workspace = true } 31 | tracing-appender = { workspace = true } 32 | time = { workspace = true } 33 | vcz-lib = { workspace = true } 34 | vcz-ui = { workspace = true } 35 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use thiserror::Error; 4 | use tokio::sync::mpsc; 5 | 6 | use crate::{action::Action, tui::Event}; 7 | 8 | impl From> for Error { 9 | fn from(_value: mpsc::error::SendError) -> Self { 10 | Self::SendErrorAction 11 | } 12 | } 13 | 14 | #[derive(Error, Debug)] 15 | pub enum Error { 16 | #[error("Could not send message to UI")] 17 | SendErrorFr, 18 | 19 | #[error("IO error")] 20 | IO(#[from] std::io::Error), 21 | 22 | #[error("Daemon Error")] 23 | DaemonError(#[from] vcz_lib::error::Error), 24 | 25 | #[error("Could not send message to TCP socket: `{0}`")] 26 | SendErrorTcp(String), 27 | 28 | #[error("Could not send message")] 29 | SendError(#[from] mpsc::error::SendError), 30 | 31 | #[error("Could not send action")] 32 | SendErrorAction, 33 | 34 | #[error("Could not receive message")] 35 | RecvError, 36 | 37 | #[error("The daemon is not running on the given addr: {0}")] 38 | DaemonNotRunning(SocketAddr), 39 | } 40 | -------------------------------------------------------------------------------- /tape.tape: -------------------------------------------------------------------------------- 1 | # This is a vhs script. See https://github.com/charmbracelet/vhs for more info. 2 | # To run this script, install vhs and run `vhs ./examples/block.tape` 3 | 4 | Set Theme "Catppuccin Macchiato" 5 | Output tape.gif 6 | Set Width 900 7 | Set Height 700 8 | Set Margin 15 9 | Set BorderRadius 20 10 | Set MarginFill "#6B50FF" 11 | 12 | Type "cargo run --release" 13 | Sleep 1s 14 | Enter 15 | Sleep 2s 16 | Type "t" 17 | Hide 18 | Type@0 "magnet:?xt=urn:btih:2C6B6858D61DA9543D4231A71DB4B1C9264B0685&dn=Ubuntu%2022.04%20LTS&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.bittor.pw%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fbt.xxx-tracker.com%3A2710%2Fannounce&tr=udp%3A%2F%2Fpublic.popcorn-tracker.org%3A6969%2Fannounce&tr=udp%3A%2F%2Feddie4.nl%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Fp4p.arenabg.com%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.tiny-vps.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce" 19 | Show 20 | Sleep 2s 21 | Enter 22 | Sleep 16s 23 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/extensions/extended/codec.rs: -------------------------------------------------------------------------------- 1 | //! Types for the Extended protocol codec. 2 | //! BEP 10 https://www.bittorrent.org/beps/bep_0010.html 3 | 4 | use super::Extension; 5 | use crate::{ 6 | error::Error, 7 | extensions::{ExtMsg, ExtMsgHandler, ExtendedMessage}, 8 | peer::{self, Direction, Peer}, 9 | torrent::TorrentMsg, 10 | }; 11 | use bendy::encoding::ToBencode; 12 | use tokio::sync::oneshot; 13 | 14 | impl ExtMsgHandler for Peer { 15 | type Error = Error; 16 | 17 | async fn handle_msg(&mut self, msg: Extension) -> Result<(), Self::Error> { 18 | // send ours extended msg if outbound 19 | if self.state.ctx.direction == Direction::Outbound { 20 | let metadata_size = { 21 | let (otx, orx) = oneshot::channel(); 22 | self.state 23 | .ctx 24 | .torrent_ctx 25 | .tx 26 | .send(TorrentMsg::GetMetadataSize(otx)) 27 | .await?; 28 | orx.await? 29 | }; 30 | let ext = Extension::supported(metadata_size).to_bencode()?; 31 | self.feed(ExtendedMessage(Extension::ID, ext).into()).await?; 32 | } 33 | 34 | self.handle_ext(msg).await?; 35 | 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/vcz-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz-lib" 3 | version = "0.0.1" 4 | description = "A BitTorrent protocol library that powers the Vincenzo client." 5 | keywords = ["distributed", "bittorrent", "torrent", "p2p", "networking"] 6 | categories = ["network-programming"] 7 | repository = "https://github.com/gabrieldemian/vincenzo" 8 | authors = ["Gabriel Lombardo "] 9 | edition = "2024" 10 | readme = "README.md" 11 | license = "MIT" 12 | exclude = ["*.log"] 13 | 14 | [dependencies] 15 | int-enum = { workspace = true } 16 | bincode = { workspace = true } 17 | rkyv = { workspace = true } 18 | lru = { workspace = true } 19 | bendy = { workspace = true } 20 | rayon = { workspace = true } 21 | memmap2 = { workspace = true } 22 | bitvec = { workspace = true } 23 | bytes = { workspace = true } 24 | clap = { workspace = true } 25 | config = { workspace = true } 26 | dirs = { version = "6.0.0" } 27 | futures = { workspace = true } 28 | hashbrown = { workspace = true } 29 | hex = { workspace = true } 30 | magnet-url = { workspace = true } 31 | rand = { workspace = true } 32 | serde = { workspace = true } 33 | sha1 = { workspace = true } 34 | thiserror = { workspace = true } 35 | tokio = { workspace = true } 36 | tokio-util = { workspace = true } 37 | toml = { workspace = true } 38 | tracing = { workspace = true } 39 | urlencoding = { workspace = true } 40 | vcz-macros = { workspace = true } 41 | signal-hook = { workspace = true } 42 | signal-hook-tokio = { workspace = true } 43 | 44 | [features] 45 | debug = [] 46 | -------------------------------------------------------------------------------- /crates/utp/src/listener.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::{io, sync::Arc}; 3 | use tokio::net::{ToSocketAddrs, UdpSocket}; 4 | 5 | #[derive(Debug)] 6 | pub struct UtpListener { 7 | socket: Arc, 8 | } 9 | 10 | impl UtpListener { 11 | /// Creates a new UTP socket bound to the specified address 12 | pub async fn bind(addr: A) -> io::Result { 13 | let socket = Arc::new(UdpSocket::bind(addr).await?); 14 | Ok(UtpListener { socket }) 15 | } 16 | 17 | /// Accepts a new incoming connection from this listener. 18 | /// 19 | /// This function will yield once a new TCP connection is established. When 20 | /// established, the corresponding [`UtpStream`] and the remote peer's 21 | /// address will be returned. 22 | pub async fn accept(&self) -> io::Result { 23 | let mut buf = [0u8; HEADER_LEN]; 24 | let (_read, sender) = self.socket.recv_from(&mut buf).await?; 25 | 26 | let mut stream = { 27 | UtpStream { 28 | sent_packets: Default::default(), 29 | socket: self.socket.clone(), 30 | incoming_buf: Vec::new(), 31 | write_buf: Vec::default(), 32 | utp_header: UtpHeader::new(), 33 | state: ConnectionState::Closed, 34 | cc: CongestionControl::new(), 35 | } 36 | }; 37 | 38 | stream.socket.connect(sender).await?; 39 | stream.handle_syn(&Header::from_bytes(&buf)?); 40 | 41 | Ok(stream) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/palette.rs: -------------------------------------------------------------------------------- 1 | use ratatui::prelude::*; 2 | use std::sync::LazyLock; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct AppStyle { 6 | pub base_style: Style, 7 | pub highlight_bg: Style, 8 | pub highlight_fg: Style, 9 | 10 | pub base: Color, 11 | pub primary: Color, 12 | pub blue: Color, 13 | pub green: Color, 14 | pub purple: Color, 15 | pub yellow: Color, 16 | pub success: Color, 17 | pub error: Color, 18 | pub warning: Color, 19 | } 20 | 21 | impl Default for AppStyle { 22 | fn default() -> Self { 23 | let base_color = Color::Gray; 24 | let blue = Color::from_u32(0x0063A7FF); 25 | let green = Color::from_u32(0x006EEB83); 26 | let red = Color::from_u32(0x00f86624); 27 | let yellow = Color::from_u32(0x00f9c80e); 28 | let purple = Color::from_u32(0x00cd57ff); 29 | 30 | Self { 31 | base: base_color, 32 | primary: blue, 33 | blue, 34 | green, 35 | purple, 36 | yellow, 37 | 38 | success: green, 39 | error: red, 40 | warning: yellow, 41 | 42 | base_style: Style::default().fg(base_color), 43 | highlight_fg: Style::default().fg(blue), 44 | highlight_bg: Style::default().bg(blue).fg(base_color), 45 | } 46 | } 47 | } 48 | 49 | pub static PALETTE: LazyLock = LazyLock::new(AppStyle::default); 50 | 51 | impl AppStyle { 52 | pub fn new() -> Self { 53 | Self::default() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | rust-overlay.url = "github:oxalica/rust-overlay"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = 9 | { 10 | nixpkgs, 11 | rust-overlay, 12 | flake-utils, 13 | ... 14 | }: 15 | flake-utils.lib.eachDefaultSystem ( 16 | system: 17 | let 18 | overlays = [ (import rust-overlay) ]; 19 | pkgs = import nixpkgs { 20 | inherit system overlays; 21 | }; 22 | in 23 | with pkgs; 24 | { 25 | devShells.default = mkShell { 26 | shellHook = '' 27 | export DOWNLOAD_DIR="$HOME/vincenzo/test-files"; 28 | # export XDG_DOWNLOAD_DIR="$HOME/downloads"; 29 | # export XDG_CONFIG_HOME="$HOME/.config"; 30 | # export XDG_STATE_HOME="$HOME/.local/state"; 31 | # export XDG_DATA_HOME="$HOME/.local/share"; 32 | ''; 33 | buildInputs = [ 34 | (writeShellScriptBin "writedump" ''sudo tcpdump -i CloudflareWARP -XX 'tcp and port not https' -w dump.pcap'') 35 | rustup 36 | cargo-bloat 37 | cargo-flamegraph 38 | cargo-unused-features 39 | cargo-udeps 40 | taplo 41 | trippy 42 | netscanner 43 | tcpdump 44 | pkg-config 45 | glib 46 | (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) 47 | ]; 48 | }; 49 | } 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | # (optional) Path to changelog. 19 | # changelog: CHANGELOG.md 20 | # (required) GitHub token for creating GitHub Releases. 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | upload-assets: 24 | needs: create-release 25 | strategy: 26 | matrix: 27 | include: 28 | - target: aarch64-unknown-linux-gnu 29 | os: ubuntu-latest 30 | - target: aarch64-apple-darwin 31 | os: macos-latest 32 | - target: x86_64-unknown-linux-gnu 33 | os: ubuntu-latest 34 | - target: x86_64-apple-darwin 35 | os: macos-latest 36 | - target: x86_64-unknown-freebsd 37 | os: ubuntu-latest 38 | - target: x86_64-pc-windows-msvc 39 | os: windows-latest 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Install cross-compilation tools 44 | uses: taiki-e/setup-cross-toolchain-action@v1 45 | with: 46 | target: ${{ matrix.target }} 47 | if: startsWith(matrix.os, 'ubuntu') 48 | - uses: taiki-e/upload-rust-binary-action@v1 49 | with: 50 | # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. 51 | # Note that glob pattern is not supported yet. 52 | bin: vcz,vcz_ui,vczd 53 | archive: vcz-$target 54 | # (optional) Target triple, default is host triple. 55 | target: ${{ matrix.target }} 56 | # (required) GitHub token for uploading assets to GitHub Releases. 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | -------------------------------------------------------------------------------- /crates/vcz/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use magnet_url::Magnet; 4 | use tokio::{spawn, sync::mpsc}; 5 | use tracing::Level; 6 | use tracing_appender::rolling::{RollingFileAppender, Rotation}; 7 | use tracing_subscriber::FmtSubscriber; 8 | use vcz_lib::{ 9 | config::Config, 10 | daemon::Daemon, 11 | disk::{Disk, DiskMsg, ReturnToDisk}, 12 | error::Error, 13 | }; 14 | 15 | use vcz_ui::{action::Action, app::App}; 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Error> { 19 | let config = Arc::new(Config::load()?); 20 | let tmp = std::env::temp_dir(); 21 | 22 | let time = std::time::SystemTime::now(); 23 | let timestamp = 24 | time.duration_since(std::time::UNIX_EPOCH).unwrap().as_millis(); 25 | 26 | let file_appender = RollingFileAppender::new( 27 | Rotation::NEVER, 28 | tmp, 29 | format!("vcz-{timestamp}.log"), 30 | ); 31 | let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); 32 | 33 | let subscriber = FmtSubscriber::builder() 34 | .without_time() 35 | .with_target(false) 36 | .with_max_level(Level::INFO) 37 | .with_writer(non_blocking) 38 | .finish(); 39 | 40 | tracing::subscriber::set_global_default(subscriber) 41 | .expect("setting default subscriber failed"); 42 | 43 | tracing::info!("config: {config:?}"); 44 | 45 | let (disk_tx, disk_rx) = mpsc::channel::(512); 46 | let (free_tx, free_rx) = mpsc::unbounded_channel::(); 47 | 48 | let mut daemon = Daemon::new(config.clone(), disk_tx.clone(), free_tx); 49 | let mut disk = Disk::new( 50 | config.clone(), 51 | daemon.ctx.clone(), 52 | disk_tx, 53 | disk_rx, 54 | free_rx, 55 | ); 56 | 57 | let disk_handle = spawn(async move { disk.run().await }); 58 | let daemon_handle = spawn(async move { daemon.run().await }); 59 | 60 | let (fr_tx, fr_rx) = mpsc::unbounded_channel(); 61 | 62 | // If the user passed a magnet through the CLI, 63 | // start this torrent immediately 64 | if let Some(magnet) = &config.magnet { 65 | let magnet = Magnet::new(magnet)?; 66 | let _ = fr_tx.send(Action::NewTorrent(magnet)); 67 | } 68 | 69 | // Start and run the terminal UI 70 | let mut app = App::new(config.clone(), fr_tx.clone()); 71 | 72 | tokio::join!(daemon_handle, disk_handle, app.run(fr_rx)).0? 73 | } 74 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.release] 2 | codegen-units = 1 3 | debug = 1 4 | incremental = false 5 | lto = true 6 | opt-level = "z" 7 | overflow-checks = false 8 | panic = "abort" 9 | strip = true 10 | 11 | [workspace] 12 | members = [ 13 | "crates/vcz-daemon", 14 | "crates/vcz-ui", 15 | "crates/vcz-lib", 16 | "crates/vcz-test", 17 | "crates/vcz-macros", 18 | "crates/vcz", 19 | "crates/utp", 20 | ] 21 | 22 | resolver = "2" 23 | 24 | [workspace.dependencies] 25 | int-enum = "1.2.0" 26 | rkyv = { version = "0.8.11", features = ["unaligned", "big_endian"] } 27 | bincode = { version = "=2.0.1" } 28 | lru = "0.16.2" 29 | bendy = { version = "0.5.0", features = ["std", "serde"] } 30 | bitvec = "1.0.1" 31 | bytes = "1.10.1" 32 | clap = { version = "4.5.49", features = ["derive"] } 33 | config = "0.15.18" 34 | futures = "0.3.31" 35 | hashbrown = "0.16.0" 36 | hex = "0.4.3" 37 | magnet-url = "3.0.0" 38 | rand = "0.9.2" 39 | rayon = "1.11.0" 40 | memmap2 = "0.9.8" 41 | ratatui = { version = "0.29.0" } 42 | serde = { version = "1.0.228", features = ["derive"] } 43 | signal-hook = { version = "0.3.18" } 44 | signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } 45 | sha1 = { version = "0.10.6", features = ["sha1-asm"] } 46 | thiserror = "2.0.17" 47 | time = "0.3.44" 48 | tokio = { version = "1.48.0", features = [ 49 | "rt", 50 | "fs", 51 | "tracing", 52 | "time", 53 | "macros", 54 | "rt-multi-thread", 55 | "sync", 56 | "io-std", 57 | "io-util", 58 | "bytes", 59 | "net", 60 | ] } 61 | tokio-util = { version = "0.7.16", features = ["codec"] } 62 | toml = "0.9.8" 63 | tracing = "0.1.41" 64 | tracing-appender = "0.2.3" 65 | tracing-subscriber = { version = "0.3.20", features = ["time"] } 66 | urlencoding = "2.1.3" 67 | vcz = { path = "crates/vcz" } 68 | vcz-daemon = { path = "crates/vcz-daemon" } 69 | vcz-ui = { path = "crates/vcz-ui" } 70 | vcz-lib = { path = "crates/vcz-lib" } 71 | vcz-macros = { path = "crates/vcz-macros" } 72 | 73 | [workspace.metadata.spellcheck] 74 | config = "spellcheck.toml" 75 | 76 | # [workspace.lints.rust] 77 | # unexpected_cfgs = { level = "warn", check-cfg = [ 78 | # 'cfg(fuzzing)', 79 | # 'cfg(loom)', 80 | # 'cfg(mio_unsupported_force_poll_poll)', 81 | # 'cfg(tokio_allow_from_blocking_fd)', 82 | # 'cfg(tokio_internal_mt_counters)', 83 | # 'cfg(tokio_no_parking_lot)', 84 | # 'cfg(tokio_no_tuning_tests)', 85 | # 'cfg(tokio_taskdump)', 86 | # 'cfg(tokio_unstable)', 87 | # 'cfg(target_os, values("cygwin"))', 88 | # ] } 89 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | schedule: 8 | - cron: '00 01 * * *' 9 | 10 | jobs: 11 | check: 12 | name: Check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v4 17 | 18 | - name: Install stable toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: stable 23 | override: true 24 | 25 | - uses: Swatinem/rust-cache@v1 26 | 27 | - name: Run cargo check 28 | uses: actions-rs/cargo@v1 29 | with: 30 | command: check 31 | 32 | test: 33 | name: Test Suite 34 | strategy: 35 | matrix: 36 | os: [ubuntu-latest, macos-latest, windows-latest] 37 | rust: [stable] 38 | runs-on: ${{ matrix.os }} 39 | steps: 40 | - name: Checkout sources 41 | uses: actions/checkout@v4 42 | 43 | - name: Install stable toolchain 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | profile: minimal 47 | toolchain: ${{ matrix.rust }} 48 | override: true 49 | 50 | - uses: Swatinem/rust-cache@v1 51 | 52 | - name: Run cargo test 53 | uses: actions-rs/cargo@v1 54 | with: 55 | command: test 56 | 57 | 58 | lints: 59 | name: Lints 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout sources 63 | uses: actions/checkout@v2 64 | with: 65 | submodules: true 66 | 67 | - name: Install stable toolchain 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | profile: minimal 71 | toolchain: stable 72 | override: true 73 | components: rustfmt, clippy 74 | 75 | - uses: Swatinem/rust-cache@v1 76 | 77 | - name: Run cargo fmt 78 | uses: actions-rs/cargo@v1 79 | with: 80 | command: fmt 81 | args: --all -- --check 82 | 83 | - name: Run cargo clippy 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: clippy 87 | args: -- -D warnings 88 | 89 | - name: Run rustdoc lints 90 | uses: actions-rs/cargo@v1 91 | env: 92 | RUSTDOCFLAGS: "-D missing_docs -D rustdoc::missing_doc_code_examples" 93 | with: 94 | command: doc 95 | args: --workspace --all-features --no-deps --document-private-items 96 | -------------------------------------------------------------------------------- /crates/vcz-test/tests/unchoke-algorithm.rs: -------------------------------------------------------------------------------- 1 | #![feature(ip_as_octets)] 2 | 3 | use std::{sync::atomic::Ordering, time::Duration}; 4 | use tokio::{sync::oneshot, time::sleep}; 5 | use vcz_lib::{error::Error, torrent::TorrentMsg}; 6 | 7 | mod common; 8 | 9 | /// Test that the unchoke algorithm works correctly. 10 | #[tokio::test] 11 | async fn unchoke_algorithm() -> Result<(), Error> { 12 | let (res, cleanup) = common::setup_seeder_client().await?; 13 | let (.., storrent, _speer) = res.0; 14 | let (.., l1peer) = res.1; 15 | let (.., l2peer) = res.2; 16 | let (.., l3peer) = res.3; 17 | let (.., l4peer) = res.4; 18 | 19 | l1peer.peer_interested.store(true, Ordering::Relaxed); 20 | l2peer.peer_interested.store(true, Ordering::Relaxed); 21 | l3peer.peer_interested.store(true, Ordering::Relaxed); 22 | l4peer.peer_interested.store(true, Ordering::Relaxed); 23 | 24 | l1peer.counter.record_download(100_000); 25 | l2peer.counter.record_download(200_000); 26 | l3peer.counter.record_download(300_000); 27 | l4peer.counter.record_download(400_000); 28 | l1peer.counter.update_rates(); 29 | l2peer.counter.update_rates(); 30 | l3peer.counter.update_rates(); 31 | l4peer.counter.update_rates(); 32 | 33 | storrent.send(TorrentMsg::UnchokeAlgorithm).await?; 34 | sleep(Duration::from_millis(30)).await; 35 | 36 | let (otx, orx) = oneshot::channel(); 37 | storrent.send(TorrentMsg::GetUnchokedPeers(otx)).await?; 38 | let unchoked = orx.await?; 39 | 40 | assert_eq!(unchoked[0].id, l4peer.id); 41 | assert_eq!(unchoked[1].id, l3peer.id); 42 | assert_eq!(unchoked[2].id, l2peer.id); 43 | 44 | // l4 is no longer in the top3, it should be replaced. 45 | 46 | l1peer.counter.record_download(900_000_000); 47 | l2peer.counter.record_download(800_000_000); 48 | l3peer.counter.record_download(700_000_000); 49 | l4peer.counter.record_download(100_000); 50 | l1peer.counter.update_rates(); 51 | l2peer.counter.update_rates(); 52 | l3peer.counter.update_rates(); 53 | l4peer.counter.update_rates(); 54 | 55 | storrent.send(TorrentMsg::UnchokeAlgorithm).await?; 56 | sleep(Duration::from_millis(30)).await; 57 | 58 | let (otx, orx) = oneshot::channel(); 59 | storrent.send(TorrentMsg::GetUnchokedPeers(otx)).await?; 60 | let unchoked = orx.await?; 61 | 62 | assert_eq!(unchoked[0].id, l1peer.id); 63 | assert_eq!(unchoked[1].id, l2peer.id); 64 | assert_eq!(unchoked[2].id, l3peer.id); 65 | 66 | cleanup(); 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /crates/vcz-test/tests/common/peer.rs: -------------------------------------------------------------------------------- 1 | //! Helpers to build Peers. 2 | 3 | use tokio::sync::oneshot; 4 | use vcz_lib::torrent::TorrentMsg; 5 | 6 | use super::*; 7 | use std::marker::PhantomData; 8 | 9 | pub(crate) trait PeerBuilderState {} 10 | 11 | pub(crate) struct Seeder {} 12 | impl PeerBuilderState for Seeder {} 13 | 14 | pub(crate) struct Leecher {} 15 | impl PeerBuilderState for Leecher {} 16 | 17 | pub(crate) struct PeerBuilder { 18 | s: PhantomData, 19 | } 20 | 21 | impl PeerBuilder { 22 | pub(crate) async fn build( 23 | self, 24 | ) -> Result<(PeerId, mpsc::Sender, mpsc::Sender), Error> 25 | { 26 | let (mut disk, mut daemon, metainfo) = 27 | setup_incomplete_torrent().await?; 28 | let p = get_p(&daemon); 29 | disk.set_piece_strategy( 30 | &metainfo.info.info_hash, 31 | PieceStrategy::Sequential, 32 | )?; 33 | let disk_tx = disk.tx.clone(); 34 | spawn(async move { daemon.run().await }); 35 | let info_hash = metainfo.info.info_hash.clone(); 36 | disk.new_torrent_metainfo(metainfo).await?; 37 | let torrent_tx = disk.torrent_ctxs.get(&info_hash).unwrap().tx.clone(); 38 | spawn(async move { disk.run().await }); 39 | Ok((p.0, disk_tx, torrent_tx)) 40 | } 41 | 42 | pub(crate) fn leecher() -> PeerBuilder { 43 | PeerBuilder { s: PhantomData } 44 | } 45 | } 46 | 47 | impl PeerBuilder { 48 | pub(crate) async fn build( 49 | self, 50 | ) -> Result<(PeerId, mpsc::Sender, mpsc::Sender), Error> 51 | { 52 | let (mut disk, mut daemon, metainfo) = setup_complete_torrent().await?; 53 | let p = get_p(&daemon); 54 | disk.set_piece_strategy( 55 | &metainfo.info.info_hash, 56 | PieceStrategy::Sequential, 57 | )?; 58 | let disk_tx = disk.tx.clone(); 59 | spawn(async move { daemon.run().await }); 60 | spawn(async move { disk.run().await }); 61 | let (otx, orx) = oneshot::channel(); 62 | disk_tx 63 | .send(DiskMsg::GetTorrentCtx(metainfo.info.info_hash.clone(), otx)) 64 | .await?; 65 | let torrent_ctx = orx.await?.unwrap(); 66 | Ok((p.0, disk_tx, torrent_ctx.tx.clone())) 67 | } 68 | 69 | pub(crate) fn seeder() -> PeerBuilder { 70 | PeerBuilder { s: PhantomData } 71 | } 72 | } 73 | 74 | #[inline] 75 | fn get_p(d: &Daemon) -> (PeerId, PeerInfo) { 76 | let addr = SocketAddr::V4(SocketAddrV4::new( 77 | Ipv4Addr::new(127, 0, 0, 1), 78 | d.config.local_peer_port, 79 | )); 80 | ( 81 | d.ctx.local_peer_id.clone(), 82 | PeerInfo { connection_id: rand::random(), key: rand::random(), addr }, 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /crates/vcz-daemon/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::SinkExt; 4 | use magnet_url::Magnet; 5 | use tokio::{ 6 | net::{TcpListener, TcpStream}, 7 | spawn, 8 | sync::mpsc, 9 | }; 10 | use tokio_util::codec::Framed; 11 | use tracing::Level; 12 | use tracing_subscriber::FmtSubscriber; 13 | use vcz_lib::{ 14 | config::Config, 15 | daemon::Daemon, 16 | daemon_wire::{DaemonCodec, Message}, 17 | disk::{Disk, DiskMsg, ReturnToDisk}, 18 | error::Error, 19 | }; 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Error> { 23 | let config = Arc::new(Config::load()?); 24 | 25 | let subscriber = FmtSubscriber::builder() 26 | .without_time() 27 | .with_target(false) 28 | .with_file(false) 29 | .with_max_level(Level::INFO) 30 | .finish(); 31 | 32 | tracing::subscriber::set_global_default(subscriber) 33 | .expect("setting default subscriber failed"); 34 | 35 | tracing::info!("config: {config:?}"); 36 | 37 | let is_daemon_running = 38 | TcpListener::bind(config.daemon_addr).await.is_err(); 39 | 40 | // if the daemon is not running, run it 41 | if !is_daemon_running { 42 | let (disk_tx, disk_rx) = mpsc::channel::(512); 43 | let (free_tx, free_rx) = mpsc::unbounded_channel::(); 44 | 45 | let mut daemon = Daemon::new(config.clone(), disk_tx.clone(), free_tx); 46 | let mut disk = 47 | Disk::new(config.clone(), daemon.ctx.clone(), disk_tx, disk_rx, free_rx); 48 | 49 | let disk_handle = spawn(async move { disk.run().await }); 50 | let daemon_handle = spawn(async move { daemon.run().await }); 51 | disk_handle.await??; 52 | daemon_handle.await??; 53 | } 54 | 55 | // Now that the daemon is running on a process, 56 | // the user can send commands using CLI flags, 57 | // using a different terminal, and we want 58 | // to listen to these flags and send messages to Daemon. 59 | // 60 | // 1. Create a TCP connection to Daemon 61 | let Ok(socket) = TcpStream::connect(config.daemon_addr).await else { 62 | return Ok(()); 63 | }; 64 | 65 | let mut socket = Framed::new(socket, DaemonCodec); 66 | 67 | // 2. Fire the corresponding message of a CLI flag. 68 | // 69 | // add a a new torrent to Daemon 70 | if let Some(magnet) = &config.magnet { 71 | let magnet = Magnet::new(magnet)?; 72 | socket.send(Message::NewTorrent(magnet)).await?; 73 | } 74 | 75 | if config.stats { 76 | socket.send(Message::PrintTorrentStatus).await?; 77 | } 78 | 79 | if config.quit { 80 | socket.send(Message::Quit).await?; 81 | } 82 | 83 | if let Some(id) = &config.pause { 84 | let id = hex::decode(id); 85 | if let Ok(id) = id { 86 | socket.send(Message::TogglePause(id.try_into().unwrap())).await?; 87 | } 88 | } 89 | 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/tracker/event.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{ 2 | Archive, Deserialize, Place, Portable, Serialize, 3 | bytecheck::{CheckBytes, InvalidEnumDiscriminantError, Verify}, 4 | primitive::ArchivedU32, 5 | rancor::{Fallible, Source, fail}, 6 | traits::NoUndef, 7 | }; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 10 | pub enum Event { 11 | None = 0, 12 | Completed = 1, 13 | #[default] 14 | Started = 2, 15 | Stopped = 3, 16 | } 17 | 18 | #[derive(CheckBytes, Portable, Debug, PartialEq)] 19 | #[bytecheck(crate = rkyv::bytecheck, verify)] 20 | #[repr(C)] 21 | pub struct ArchivedEvent(ArchivedU32); 22 | 23 | unsafe impl NoUndef for ArchivedEvent {} 24 | 25 | impl PartialEq for ArchivedEvent { 26 | fn eq(&self, other: &Event) -> bool { 27 | self.0 == (*other) as u32 28 | } 29 | } 30 | 31 | impl ArchivedEvent { 32 | // Internal fallible conversion back to the original enum 33 | fn try_to_native(&self) -> Option { 34 | Some(match self.0.to_native() { 35 | 0 => Event::None, 36 | 1 => Event::Completed, 37 | 2 => Event::Started, 38 | 3 => Event::Stopped, 39 | _ => return None, 40 | }) 41 | } 42 | 43 | // Public infallible conversion back to the original enum 44 | pub fn to_native(&self) -> Event { 45 | unsafe { self.try_to_native().unwrap_unchecked() } 46 | } 47 | } 48 | 49 | unsafe impl Verify for ArchivedEvent 50 | where 51 | C::Error: Source, 52 | { 53 | // verify runs after all of the fields have been checked 54 | fn verify(&self, _: &mut C) -> Result<(), C::Error> { 55 | // Use the internal conversion to try to convert back 56 | if self.try_to_native().is_none() { 57 | // Return an error if it fails (i.e. the discriminant did not match 58 | // any valid discriminants) 59 | fail!(InvalidEnumDiscriminantError { 60 | enum_name: "ArchivedEvent", 61 | invalid_discriminant: self.0.to_native(), 62 | }) 63 | } 64 | Ok(()) 65 | } 66 | } 67 | 68 | impl Archive for Event { 69 | type Archived = ArchivedEvent; 70 | type Resolver = (); 71 | 72 | fn resolve(&self, _: Self::Resolver, out: Place) { 73 | // Convert MyEnum -> u32 -> ArchivedU32 and write to `out` 74 | out.write(ArchivedEvent((*self as u32).into())); 75 | } 76 | } 77 | 78 | // Serialization is a no-op because there's no out-of-line data 79 | impl Serialize for Event { 80 | fn serialize( 81 | &self, 82 | _: &mut S, 83 | ) -> Result::Error> { 84 | Ok(()) 85 | } 86 | } 87 | 88 | // Deserialization just calls the public conversion and returns the result 89 | impl Deserialize for ArchivedEvent { 90 | fn deserialize(&self, _: &mut D) -> Result::Error> { 91 | Ok(self.to_native()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions 2 | 3 | /// transform bytes into a human readable format. 4 | pub fn to_human_readable(mut n: f64) -> String { 5 | let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 6 | let delimiter = 1000_f64; 7 | 8 | if n < delimiter { 9 | // for bytes, format without trailing zeros 10 | if n.fract() == 0.0 { 11 | return format!("{:.0} {}", n, units[0]); 12 | } else { 13 | let formatted = format!("{:.2}", n); 14 | let trimmed = formatted.trim_end_matches('0').trim_end_matches('.'); 15 | return format!("{} {}", trimmed, units[0]); 16 | } 17 | } 18 | 19 | let mut u: i32 = 0; 20 | let r = 10_f64; 21 | 22 | while (n * r).round() / r >= delimiter && u < (units.len() as i32) - 1 { 23 | n /= delimiter; 24 | u += 1; 25 | } 26 | 27 | // for larger units, format with 2 decimal places but remove trailing zeros 28 | let formatted = format!("{:.2}", n); 29 | let trimmed = formatted.trim_end_matches('0').trim_end_matches('.'); 30 | format!("{} {}", trimmed, units[u as usize]) 31 | } 32 | 33 | /// Round to significant digits (rather than digits after the decimal). 34 | /// 35 | /// Not implemented for `f32`, because such an implementation showed precision 36 | /// glitches (e.g. `precision_f32(12300.0, 2) == 11999.999`), so for `f32` 37 | /// floats, convert to `f64` for this function and back as needed. 38 | /// 39 | /// Examples: 40 | /// ```ignore 41 | /// precision_f64(1.2300, 2) // 1.2 42 | /// precision_f64(1.2300_f64, 2) // 1.2 43 | /// precision_f64(1.2300_f32 as f64, 2) // 1.2 44 | /// precision_f64(1.2300_f32 as f64, 2) as f32 // 1.2 45 | /// ``` 46 | pub fn precision_f64(x: f64, decimals: u32) -> f64 { 47 | if x == 0. || decimals == 0 { 48 | 0. 49 | } else { 50 | let shift = decimals as i32 - x.abs().log10().ceil() as i32; 51 | let shift_factor = 10_f64.powi(shift); 52 | 53 | (x * shift_factor).round() / shift_factor 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | 61 | #[test] 62 | pub fn readable_size() { 63 | let n = 1.0; 64 | assert_eq!(to_human_readable(n), "1 B"); 65 | 66 | let n = 1.000; 67 | assert_eq!(to_human_readable(n), "1 B"); 68 | 69 | let n = 740.0; 70 | assert_eq!(to_human_readable(n), "740 B"); 71 | 72 | let n = 7_040.0; 73 | assert_eq!(to_human_readable(n), "7.04 KB"); 74 | 75 | let n = 483_740.0; 76 | assert_eq!(to_human_readable(n), "483.74 KB"); 77 | 78 | let n = 28_780_000.0; 79 | assert_eq!(to_human_readable(n), "28.78 MB"); 80 | 81 | let n = 1_950_000_000.0; 82 | assert_eq!(to_human_readable(n), "1.95 GB"); 83 | 84 | let n = u64::MAX; 85 | assert_eq!(to_human_readable(n as f64), "18.45 EB"); 86 | 87 | let n = u128::MAX; 88 | assert_eq!(to_human_readable(n as f64), "340282366920938.38 YB"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/tracker/announce.rs: -------------------------------------------------------------------------------- 1 | use super::{action::Action, event::Event}; 2 | use crate::{ 3 | error::Error, 4 | peer::PeerId, 5 | torrent::{InfoHash, Stats}, 6 | }; 7 | use rkyv::{ 8 | Archive, Deserialize, Serialize, api::high::to_bytes_with_alloc, 9 | ser::allocator::Arena, util::AlignedVec, 10 | }; 11 | 12 | #[derive(Debug, PartialEq, Archive, Serialize, Deserialize)] 13 | #[rkyv(compare(PartialEq), derive(Debug))] 14 | pub struct Request { 15 | pub connection_id: u64, 16 | pub action: Action, 17 | pub transaction_id: u32, 18 | pub info_hash: InfoHash, 19 | pub peer_id: PeerId, 20 | pub downloaded: u64, 21 | pub left: u64, 22 | pub uploaded: u64, 23 | pub event: Event, 24 | pub ip_address: u32, // 0 default 25 | pub key: u32, 26 | pub num_want: u32, 27 | pub port: u16, 28 | pub compact: u8, 29 | } 30 | 31 | impl Request { 32 | pub const LEN: usize = 99; 33 | 34 | /// Around 5µs in release mode. 35 | pub fn serialize(&self) -> Result { 36 | let mut arena = Arena::with_capacity(Self::LEN); 37 | Ok(to_bytes_with_alloc::<_, rkyv::rancor::Error>( 38 | self, 39 | arena.acquire(), 40 | )?) 41 | } 42 | 43 | /// Around 20ns in release mode. 44 | pub fn deserialize(bytes: &[u8]) -> Result<&ArchivedRequest, Error> { 45 | if bytes.len() != Self::LEN { 46 | return Err(Error::ResponseLen); 47 | } 48 | Ok(unsafe { rkyv::access_unchecked::(bytes) }) 49 | } 50 | } 51 | 52 | #[derive(Debug, PartialEq, Serialize, Deserialize, Archive)] 53 | #[rkyv(compare(PartialEq), derive(Debug))] 54 | pub struct Response { 55 | pub action: Action, 56 | pub transaction_id: u32, 57 | pub interval: u32, 58 | pub leechers: u32, 59 | pub seeders: u32, 60 | // binary of peers that will be deserialized on 61 | } 62 | 63 | impl From for Stats { 64 | fn from(value: Response) -> Self { 65 | Self { 66 | interval: value.interval, 67 | seeders: value.seeders, 68 | leechers: value.leechers, 69 | } 70 | } 71 | } 72 | 73 | impl Response { 74 | pub const MIN_LEN: usize = 20; 75 | 76 | /// Around 237ns in release mode. 77 | pub fn serialize(&self) -> Result { 78 | let mut arena = Arena::with_capacity(Self::MIN_LEN); 79 | Ok(to_bytes_with_alloc::<_, rkyv::rancor::Error>( 80 | self, 81 | arena.acquire(), 82 | )?) 83 | } 84 | 85 | pub fn deserialize( 86 | bytes: &[u8], 87 | ) -> Result<(&ArchivedResponse, &[u8]), Error> { 88 | if bytes.len() < Response::MIN_LEN { 89 | return Err(Error::ResponseLen); 90 | } 91 | Ok(( 92 | unsafe { 93 | rkyv::access_unchecked::( 94 | &bytes[..Self::MIN_LEN], 95 | ) 96 | }, 97 | &bytes[Self::MIN_LEN..], 98 | )) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/vcz-test/tests/requests.rs: -------------------------------------------------------------------------------- 1 | #![feature(ip_as_octets)] 2 | 3 | use std::{sync::atomic::Ordering, time::Duration}; 4 | use tokio::{sync::oneshot, time::sleep}; 5 | use vcz_lib::{ 6 | disk::DiskMsg, 7 | error::Error, 8 | extensions::{BLOCK_LEN, BlockInfo}, 9 | peer::PeerMsg, 10 | torrent::TorrentMsg, 11 | }; 12 | 13 | mod common; 14 | 15 | /// Simulate a local leecher requesting blocks from a seeder. 16 | /// 17 | /// The torrent has 6 block infos, the disk must send unique block infos for 18 | /// each request. 19 | #[tokio::test] 20 | async fn request_block() -> Result<(), Error> { 21 | let (leecher, seeder, cleanup) = common::setup_pair().await?; 22 | let (otx, orx) = oneshot::channel(); 23 | let (ldisk_tx, _ltorrent, leecher) = leecher; 24 | let (_sdisk_tx, storrent, seeder) = seeder; 25 | 26 | // ! leecher and seeder are in switched perspectives. 27 | // but the torrent txs are in the right perspective. 28 | 29 | // the leecher will run the interested allgorithm. 30 | seeder.tx.send(PeerMsg::InterestedAlgorithm).await?; 31 | sleep(Duration::from_millis(20)).await; 32 | 33 | storrent.send(TorrentMsg::UnchokeAlgorithm).await?; 34 | sleep(Duration::from_millis(20)).await; 35 | 36 | // seeder is not choking the leecher 37 | assert!(!leecher.am_choking.load(Ordering::Acquire)); 38 | assert!(!leecher.am_interested.load(Ordering::Acquire)); 39 | assert!(!seeder.peer_choking.load(Ordering::Acquire)); 40 | assert!(seeder.am_interested.load(Ordering::Acquire)); 41 | 42 | ldisk_tx 43 | .send(DiskMsg::RequestBlocks { 44 | peer_id: seeder.id.clone(), 45 | recipient: otx, 46 | qnt: 3, 47 | }) 48 | .await?; 49 | 50 | let blocks = orx.await?; 51 | 52 | assert_eq!( 53 | blocks, 54 | vec![ 55 | BlockInfo { index: 0, begin: 0, len: BLOCK_LEN }, 56 | BlockInfo { index: 1, begin: 0, len: BLOCK_LEN }, 57 | BlockInfo { index: 2, begin: 0, len: BLOCK_LEN }, 58 | ] 59 | ); 60 | 61 | let (otx, orx) = oneshot::channel(); 62 | ldisk_tx 63 | .send(DiskMsg::RequestBlocks { 64 | peer_id: seeder.id.clone(), 65 | recipient: otx, 66 | qnt: 3, 67 | }) 68 | .await?; 69 | 70 | let blocks = orx.await?; 71 | 72 | assert_eq!( 73 | blocks, 74 | vec![ 75 | BlockInfo { index: 3, begin: 0, len: BLOCK_LEN }, 76 | BlockInfo { index: 4, begin: 0, len: BLOCK_LEN }, 77 | BlockInfo { index: 5, begin: 0, len: BLOCK_LEN }, 78 | ] 79 | ); 80 | 81 | let (otx, orx) = oneshot::channel(); 82 | ldisk_tx 83 | .send(DiskMsg::RequestBlocks { 84 | peer_id: seeder.id.clone(), 85 | recipient: otx, 86 | qnt: 3, 87 | }) 88 | .await?; 89 | 90 | let blocks = orx.await?; 91 | 92 | assert!( 93 | blocks.is_empty(), 94 | "disk must not have any more block infos to be requested" 95 | ); 96 | 97 | cleanup(); 98 | 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/tracker/action.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{ 2 | Archive, Deserialize, Place, Portable, Serialize, 3 | bytecheck::{CheckBytes, InvalidEnumDiscriminantError, Verify}, 4 | primitive::ArchivedU32, 5 | rancor::{Fallible, Source, fail}, 6 | traits::NoUndef, 7 | }; 8 | 9 | #[derive(Debug, Clone, Copy, PartialEq, Default)] 10 | pub enum Action { 11 | Connect = 0, 12 | #[default] 13 | Announce = 1, 14 | Scrape = 2, 15 | } 16 | 17 | impl TryFrom for Action { 18 | type Error = u32; 19 | fn try_from(value: u32) -> Result { 20 | Ok(match value { 21 | 0 => Action::Connect, 22 | 1 => Action::Announce, 23 | 2 => Action::Scrape, 24 | _ => return Err(value), 25 | }) 26 | } 27 | } 28 | 29 | #[derive(CheckBytes, Portable, Debug, PartialEq)] 30 | #[bytecheck(crate = rkyv::bytecheck, verify)] 31 | #[repr(C)] 32 | pub struct ArchivedAction(ArchivedU32); 33 | 34 | unsafe impl NoUndef for ArchivedAction {} 35 | 36 | impl PartialEq for ArchivedAction { 37 | fn eq(&self, other: &Action) -> bool { 38 | self.0 == (*other) as u32 39 | } 40 | } 41 | 42 | impl ArchivedAction { 43 | // Internal fallible conversion back to the original enum 44 | fn try_to_native(&self) -> Option { 45 | Some(match self.0.to_native() { 46 | 0 => Action::Connect, 47 | 1 => Action::Announce, 48 | 2 => Action::Scrape, 49 | _ => return None, 50 | }) 51 | } 52 | 53 | // Public infallible conversion back to the original enum 54 | pub fn to_native(&self) -> Action { 55 | unsafe { self.try_to_native().unwrap_unchecked() } 56 | } 57 | } 58 | 59 | unsafe impl Verify for ArchivedAction 60 | where 61 | C::Error: Source, 62 | { 63 | // verify runs after all of the fields have been checked 64 | fn verify(&self, _: &mut C) -> Result<(), C::Error> { 65 | // Use the internal conversion to try to convert back 66 | if self.try_to_native().is_none() { 67 | // Return an error if it fails (i.e. the discriminant did not match 68 | // any valid discriminants) 69 | fail!(InvalidEnumDiscriminantError { 70 | enum_name: "ArchivedAction", 71 | invalid_discriminant: self.0.to_native(), 72 | }) 73 | } 74 | Ok(()) 75 | } 76 | } 77 | 78 | impl Archive for Action { 79 | type Archived = ArchivedAction; 80 | type Resolver = (); 81 | 82 | fn resolve(&self, _: Self::Resolver, out: Place) { 83 | // Convert Action -> u32 -> ArchivedU32 and write to `out` 84 | out.write(ArchivedAction((*self as u32).into())); 85 | } 86 | } 87 | 88 | // Serialization is a no-op because there's no out-of-line data 89 | impl Serialize for Action { 90 | fn serialize( 91 | &self, 92 | _: &mut S, 93 | ) -> Result::Error> { 94 | Ok(()) 95 | } 96 | } 97 | 98 | // Deserialization just calls the public conversion and returns the result 99 | impl Deserialize for ArchivedAction { 100 | fn deserialize(&self, _: &mut D) -> Result::Error> { 101 | Ok(self.to_native()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/extensions/core/mod.rs: -------------------------------------------------------------------------------- 1 | //! Documentation of the "TCP Wire" protocol between Peers in the network. 2 | //! Peers will follow this protocol to exchange information about torrents. 3 | 4 | mod codec; 5 | mod handshake_codec; 6 | 7 | // re-exports 8 | pub use codec::*; 9 | pub use handshake_codec::*; 10 | 11 | use bytes::Bytes; 12 | 13 | /// The default block_len that most clients support, some clients drop 14 | /// the connection on blocks larger than this value. 15 | /// 16 | /// Tha last block of a piece might be smaller. 17 | pub const BLOCK_LEN: usize = 16384; 18 | 19 | /// Protocol String (PSTR) 20 | /// Bytes of the string "BitTorrent protocol". Used during handshake. 21 | pub const PSTR: [u8; 19] = [ 22 | 66, 105, 116, 84, 111, 114, 114, 101, 110, 116, 32, 112, 114, 111, 116, 23 | 111, 99, 111, 108, 24 | ]; 25 | 26 | pub const PSTR_LEN: usize = 19; 27 | 28 | /// A Block is a subset of a Piece, 29 | /// pieces are subsets of the entire Torrent data. 30 | /// 31 | /// Blocks may overlap pieces, for example, part of a block may start at piece 32 | /// 0, but end at piece 1. 33 | /// 34 | /// When peers send data (seed) to us, they send us Blocks. 35 | /// This happens on the "Piece" message of the peer wire protocol. 36 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 37 | pub struct Block { 38 | /// The index of the piece this block belongs to. 39 | pub index: usize, 40 | 41 | /// The zero-based byte offset into the piece. 42 | pub begin: usize, 43 | 44 | /// The block's data. 16 KiB most of the times, 45 | /// but the last block of a piece *might* be smaller. 46 | pub block: Bytes, 47 | } 48 | 49 | impl Block { 50 | /// Validate the [`Block`]. Like most clients, we only support 51 | /// data <= 16kiB. 52 | #[inline] 53 | pub fn is_valid(&self) -> bool { 54 | self.block.len() <= BLOCK_LEN && self.begin <= BLOCK_LEN 55 | } 56 | } 57 | 58 | /// The representation of a [`Block`]. 59 | /// 60 | /// When we ask a peer to give us a [`Block`], we send this struct, 61 | /// using the "Request" message of the tcp wire protocol. 62 | /// 63 | /// This is almost identical to the [`Block`] struct, 64 | /// the only difference is that instead of having a `block`, 65 | /// we have a `len` representing the len of the block. 66 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 67 | pub struct BlockInfo { 68 | /// The index of the piece of which this is a block. 69 | pub index: usize, 70 | 71 | /// The zero-based byte offset into the piece. 72 | pub begin: usize, 73 | 74 | /// The block's length in bytes. <= 16 KiB 75 | pub len: usize, 76 | } 77 | 78 | impl From<&BlockInfo> for usize { 79 | fn from(val: &BlockInfo) -> Self { 80 | val.index 81 | } 82 | } 83 | 84 | impl Default for BlockInfo { 85 | fn default() -> Self { 86 | Self { index: 0, begin: 0, len: BLOCK_LEN } 87 | } 88 | } 89 | 90 | impl From<&Block> for BlockInfo { 91 | fn from(val: &Block) -> Self { 92 | BlockInfo { index: val.index, begin: val.begin, len: val.block.len() } 93 | } 94 | } 95 | 96 | impl BlockInfo { 97 | pub fn new(index: usize, begin: usize, len: usize) -> Self { 98 | Self { index, begin, len } 99 | } 100 | 101 | /// Validate the [`BlockInfo`]. Like most clients, we only support 102 | /// data <= 16kiB. 103 | #[inline] 104 | pub fn is_valid(&self) -> bool { 105 | self.len > 0 && self.len <= BLOCK_LEN && self.begin <= BLOCK_LEN 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/bitfield.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper types around Bitvec. 2 | use bitvec::{prelude::*, ptr::Const}; 3 | 4 | /// Bitfield where index = piece. 5 | pub type Bitfield = BitVec; 6 | 7 | /// Reserved bytes exchanged during handshake. 8 | type ReservedAlias = BitArray<[u8; 8], bitvec::prelude::Msb0>; 9 | 10 | #[derive(Debug, Clone, Default, Copy, PartialEq, Eq)] 11 | pub struct Reserved(pub ReservedAlias); 12 | 13 | impl From<[u8; 8]> for Reserved { 14 | fn from(value: [u8; 8]) -> Self { 15 | Self(ReservedAlias::from(value)) 16 | } 17 | } 18 | 19 | impl Reserved { 20 | /// Reserved bits of protocols that the client supports. 21 | pub fn supported() -> Reserved { 22 | // we only support the `extension protocol` 23 | Reserved(bitarr![u8, Msb0; 24 | 0, 0, 0, 0, 0, 0, 0, 0, 25 | 0, 0, 0, 0, 0, 0, 0, 0, 26 | 0, 0, 0, 0, 0, 0, 0, 0, 27 | 0, 0, 0, 0, 0, 0, 0, 0, 28 | 0, 0, 0, 0, 0, 0, 0, 0, 29 | 0, 0, 0, 1, 0, 0, 0, 0, 30 | 0, 0, 0, 0, 0, 0, 0, 0, 31 | 0, 0, 0, 0, 0, 0, 0, 0, 32 | ]) 33 | } 34 | 35 | pub fn supports_extended(&self) -> bool { 36 | unsafe { *self.0.get_unchecked(43) } 37 | } 38 | } 39 | 40 | pub trait VczBitfield { 41 | fn from_piece(piece: usize) -> Bitfield { 42 | bitvec![u8, Msb0; 0; piece] 43 | } 44 | /// Set vector to a new len, in bits. 45 | fn new_and_resize(vec: Vec, len: usize) -> Bitfield { 46 | let mut s = Bitfield::from_vec(vec); 47 | unsafe { s.set_len(len) }; 48 | s 49 | } 50 | fn safe_get(&mut self, index: usize) -> BitRef<'_, Const, u8, Msb0>; 51 | fn safe_set(&mut self, _index: usize) {} 52 | } 53 | 54 | impl VczBitfield for Bitfield { 55 | fn safe_set(&mut self, index: usize) { 56 | if self.len() <= index { 57 | self.resize(index + 1, false); 58 | } 59 | unsafe { self.set_unchecked(index, true) }; 60 | } 61 | fn safe_get(&mut self, index: usize) -> BitRef<'_, Const, u8, Msb0> { 62 | if self.len() <= index { 63 | self.resize(index + 1, false); 64 | } 65 | unsafe { self.get_unchecked(index) } 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use super::*; 72 | 73 | #[test] 74 | fn from_piece() { 75 | let bitfield = Bitfield::from_piece(1407); 76 | assert_eq!(bitfield.len(), 1407); 77 | } 78 | 79 | #[test] 80 | fn safe_set() { 81 | // 0, 1 82 | let mut bitfield = Bitfield::new_and_resize(vec![0], 2); 83 | assert_eq!(bitfield.len(), 2); 84 | // 0, 1, 2 85 | bitfield.safe_set(2); 86 | assert_eq!(bitfield.len(), 3); 87 | assert_eq!(bitfield.get(2).unwrap(), true); 88 | 89 | bitfield.safe_set(10); 90 | assert_eq!(bitfield.len(), 11); 91 | assert_eq!(bitfield.get(10).unwrap(), true); 92 | } 93 | 94 | #[test] 95 | fn safe_get() { 96 | let mut bitfield = Bitfield::new_and_resize(vec![0], 1); 97 | assert_eq!(bitfield.safe_get(10), false); 98 | assert_eq!(bitfield.len(), 11); 99 | assert_eq!(bitfield.get(10).unwrap(), false); 100 | } 101 | 102 | #[test] 103 | fn supports_ext() { 104 | let bitfield = Reserved::supported(); 105 | assert!(bitfield.supports_extended()); 106 | } 107 | 108 | #[test] 109 | fn new_and_resize() { 110 | let bitfield = Bitfield::new_and_resize(vec![0], 5); 111 | assert_eq!(bitfield.len(), 5); 112 | 113 | let bitfield = Bitfield::new_and_resize(vec![0], 7); 114 | assert_eq!(bitfield.len(), 7); 115 | 116 | let bitfield = Bitfield::new_and_resize(vec![0], 8); 117 | assert_eq!(bitfield.len(), 8); 118 | 119 | let bitfield = Bitfield::new_and_resize(vec![0, 0], 9); 120 | assert_eq!(bitfield.len(), 9); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /crates/utp/src/packet.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use int_enum::IntEnum; 4 | 5 | use super::*; 6 | 7 | /// The type field describes the type of packet. 8 | #[repr(u8)] 9 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, IntEnum)] 10 | pub(crate) enum PacketType { 11 | #[default] 12 | /// regular data packet. Socket is in connected state and has data to send. 13 | /// A Data packet always has a data payload. 14 | Data = 0, 15 | 16 | /// Finalize the connection. This is the last packet. It closes the 17 | /// connection, similar to TCP FIN flag. This connection will never have a 18 | /// sequence number greater than the sequence number in this packet. The 19 | /// socket records this sequence number as eof_pkt. This lets the socket 20 | /// wait for packets that might still be missing and arrive out of order 21 | /// even after receiving the ST_FIN packet. 22 | Fin = 1, 23 | 24 | /// State packet. Used to transmit an ACK with no data. Packets that don't 25 | /// include any payload do not increase the seq_nr. 26 | State = 2, 27 | 28 | /// Terminate connection forcefully. Similar to TCP RST flag. The remote 29 | /// host does not have any state for this connection. It is stale and should 30 | /// be terminated. 31 | Reset = 3, 32 | 33 | /// Connect SYN. Similar to TCP SYN flag, this packet initiates a 34 | /// connection. The sequence number is initialized to 1. The connection ID 35 | /// is initialized to a random number. The syn packet is special, all 36 | /// subsequent packets sent on this connection (except for re-sends of the 37 | /// SYN) are sent with the connection ID + 1. The connection ID is what 38 | /// the other end is expected to use in its responses. 39 | /// 40 | /// When receiving an SYN, the new socket should be initialized with the 41 | /// ID in the packet header. The send ID for the socket should be 42 | /// initialized to the ID + 1. The sequence number for the return channel is 43 | /// initialized to a random number. The other end expects an STATE packet 44 | /// (only an ACK) in response. 45 | Syn = 4, 46 | } 47 | 48 | #[derive(Debug, Clone, Default)] 49 | pub(super) struct SentPacket { 50 | pub packet: Packet, 51 | pub retransmit_count: u8, 52 | } 53 | 54 | /// UTP packet structure 55 | #[derive(Debug, Default, Clone, PartialEq)] 56 | pub(crate) struct Packet { 57 | pub header: Header, 58 | pub payload: Bytes, 59 | } 60 | 61 | impl From for Vec { 62 | fn from(value: Packet) -> Self { 63 | let mut v = Vec::with_capacity(20 + value.payload.len()); 64 | v.extend_from_slice(&value.header.to_bytes()); 65 | v.extend_from_slice(&value.payload); 66 | v 67 | } 68 | } 69 | 70 | impl From for Bytes { 71 | fn from(value: Packet) -> Self { 72 | let mut v = Vec::with_capacity(20 + value.payload.len()); 73 | v.extend_from_slice(&value.header.to_bytes()); 74 | v.extend_from_slice(&value.payload); 75 | Bytes::copy_from_slice(&v) 76 | } 77 | } 78 | 79 | impl Packet { 80 | pub fn new(header: Header, payload: &[u8]) -> Self { 81 | Self { header, payload: Bytes::copy_from_slice(payload) } 82 | } 83 | 84 | pub fn into_bytes(self) -> Vec { 85 | self.into() 86 | } 87 | 88 | pub fn as_bytes_mut(&self) -> BytesMut { 89 | let header = self.header.as_bytes_mut(); 90 | let payload = self.payload.clone(); 91 | let mut b = BytesMut::with_capacity(20 + payload.len()); 92 | b.extend_from_slice(&header); 93 | b.extend_from_slice(&payload); 94 | b 95 | } 96 | 97 | /// Parse a byte buffer into a UtpPacket 98 | pub fn from_bytes(data: &[u8]) -> io::Result { 99 | let header = Header::try_from(&data[0..20])?; 100 | let payload = Bytes::copy_from_slice(&data[20..]); 101 | Ok(Packet { header, payload }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/extensions/metadata/codec.rs: -------------------------------------------------------------------------------- 1 | //! Types for the metadata protocol codec. 2 | 3 | use super::{Metadata, MetadataMsgType}; 4 | use crate::{ 5 | error::Error, 6 | extensions::{ExtMsgHandler, ExtendedMessage, MetadataPiece}, 7 | peer::{self, Peer}, 8 | torrent::TorrentMsg, 9 | }; 10 | use bendy::encoding::ToBencode; 11 | use futures::SinkExt; 12 | use tokio::sync::oneshot; 13 | use tracing::{debug, warn}; 14 | 15 | impl ExtMsgHandler for Peer { 16 | type Error = Error; 17 | 18 | async fn handle_msg(&mut self, msg: Metadata) -> Result<(), Self::Error> { 19 | let Some(remote_ext_id) = 20 | self.state.extension.as_ref().and_then(|v| v.m.ut_metadata) 21 | else { 22 | warn!( 23 | "{} received extended msg but the peer doesnt support the \ 24 | extended protocol or extended metadata protocol {}", 25 | self.state.ctx.local_addr, self.state.ctx.remote_addr 26 | ); 27 | return Ok(()); 28 | }; 29 | 30 | match msg.msg_type { 31 | MetadataMsgType::Response => { 32 | debug!("< metadata res piece {}", msg.piece); 33 | 34 | self.state 35 | .req_man_meta 36 | .remove_request(&MetadataPiece(msg.piece as usize)); 37 | 38 | self.state 39 | .ctx 40 | .torrent_ctx 41 | .tx 42 | .send(TorrentMsg::DownloadedInfoPiece( 43 | msg.total_size.ok_or(Error::MessageResponse)?, 44 | msg.piece, 45 | msg.payload, 46 | )) 47 | .await?; 48 | } 49 | MetadataMsgType::Request => { 50 | debug!("< metadata req"); 51 | 52 | let (tx, rx) = oneshot::channel(); 53 | 54 | self.state 55 | .ctx 56 | .torrent_ctx 57 | .tx 58 | .send(TorrentMsg::RequestInfoPiece(msg.piece, tx)) 59 | .await?; 60 | 61 | match rx.await? { 62 | Some(info_slice) => { 63 | debug!("sending data with piece {:?}", msg.piece); 64 | debug!( 65 | "{} sending data with piece {} {}", 66 | self.state.ctx.local_addr, 67 | self.state.ctx.remote_addr, 68 | msg.piece 69 | ); 70 | 71 | let msg = Metadata::data(msg.piece, &info_slice)?; 72 | 73 | self.state 74 | .sink 75 | .send( 76 | ExtendedMessage( 77 | remote_ext_id, 78 | msg.to_bencode()?, 79 | ) 80 | .into(), 81 | ) 82 | .await?; 83 | } 84 | None => { 85 | debug!( 86 | "{} sending reject {}", 87 | self.state.ctx.local_addr, 88 | self.state.ctx.remote_addr 89 | ); 90 | 91 | let r = Metadata::reject(msg.piece).to_bencode()?; 92 | 93 | self.state 94 | .sink 95 | .send(ExtendedMessage(remote_ext_id, r).into()) 96 | .await?; 97 | } 98 | } 99 | } 100 | MetadataMsgType::Reject => { 101 | debug!("metadata reject piece {}", msg.piece); 102 | } 103 | } 104 | 105 | Ok(()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/vcz-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use darling::{FromDeriveInput, util::PathList}; 4 | use proc_macro::TokenStream; 5 | use quote::quote; 6 | use syn::{DeriveInput, parse_macro_input}; 7 | 8 | #[derive(FromDeriveInput)] 9 | #[darling(attributes(extension))] 10 | struct ExtArgs { 11 | ident: syn::Ident, 12 | id: syn::LitInt, 13 | #[darling(default)] 14 | bencoded: bool, 15 | } 16 | 17 | /// Implement ExtMsg on the type. 18 | /// 19 | /// Usage: 20 | /// 21 | /// ```ignore 22 | /// #[derive(vcz_macros::Extension)] 23 | /// #[extension(id = 3)] 24 | /// pub struct MetadataMsg; 25 | /// ``` 26 | /// 27 | /// can also implement `TryFrom` for this type 28 | /// if it implements `bendy::FromBencode` 29 | /// 30 | /// ```ignore 31 | /// #[derive(vcz_macros::Extension)] 32 | /// #[extension(id = 2, bencoded)] 33 | /// pub struct BencodedMsg; 34 | /// ``` 35 | #[proc_macro_derive(Extension, attributes(extension))] 36 | pub fn derive_extension(input: TokenStream) -> TokenStream { 37 | let input: DeriveInput = parse_macro_input!(input); 38 | let args = match ExtArgs::from_derive_input(&input) { 39 | Ok(args) => args, 40 | Err(e) => return e.write_errors().into(), 41 | }; 42 | 43 | // name of the extension 44 | let name = args.ident; 45 | let id = args.id; 46 | 47 | let ser_type = if args.bencoded { 48 | quote! { 49 | impl core::convert::TryFrom for #name { 50 | type Error = crate::error::Error; 51 | 52 | fn try_from(value: crate::extensions::ExtendedMessage) 53 | -> std::result::Result 54 | { 55 | if value.0 != #id { 56 | return std::result::Result::Err( 57 | crate::error::Error::WrongExtensionId { 58 | local: #id, 59 | received: value.0, 60 | }, 61 | ); 62 | } 63 | #name::from_bencode(&value.1).map_err(|e| e.into()) 64 | } 65 | } 66 | } 67 | } else { 68 | quote! {} 69 | }; 70 | 71 | let expanded = quote! { 72 | impl crate::extensions::ExtMsg for #name { 73 | const ID: u8 = #id; 74 | } 75 | #ser_type 76 | }; 77 | 78 | TokenStream::from(expanded) 79 | } 80 | 81 | #[derive(FromDeriveInput)] 82 | #[darling(attributes(extensions))] 83 | struct PeerExtArgs { 84 | // a list of types that implement `ExtMsg`. 85 | #[darling(flatten)] 86 | exts: PathList, 87 | } 88 | 89 | #[proc_macro_derive(Peer, attributes(extensions))] 90 | pub fn derive_peer_extensions(input: TokenStream) -> TokenStream { 91 | let input: DeriveInput = parse_macro_input!(input); 92 | let args = match PeerExtArgs::from_derive_input(&input) { 93 | Ok(args) => args, 94 | Err(e) => return e.write_errors().into(), 95 | }; 96 | let ident = input.ident; 97 | let exts = args.exts; 98 | 99 | let expanded = quote! { 100 | impl #ident { 101 | #[inline] 102 | pub async fn handle_message( 103 | &mut self, 104 | msg: crate::extensions::Core, 105 | ) 106 | -> std::result::Result<(), crate::error::Error> 107 | { 108 | match msg { 109 | crate::extensions::Core::Extended( 110 | msg @ crate::extensions::ExtendedMessage(ext_id, _) 111 | ) => { 112 | match ext_id { 113 | #( 114 | <#exts as crate::extensions::ExtMsg>::ID => { 115 | let msg: #exts = msg.try_into()?; 116 | self.handle_msg(msg).await?; 117 | } 118 | )* 119 | _ => {} 120 | } 121 | } 122 | _ => self.handle_msg(msg).await? 123 | } 124 | std::result::Result::Ok(()) 125 | } 126 | } 127 | }; 128 | 129 | TokenStream::from(expanded) 130 | } 131 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/extensions/holepunch/codec.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::Error, 3 | extensions::{ 4 | ExtMsg, ExtMsgHandler, ExtendedMessage, Holepunch, HolepunchMsgType, 5 | }, 6 | peer::{self, Peer}, 7 | torrent::TorrentMsg, 8 | }; 9 | use tokio::sync::oneshot; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct HolepunchCodec; 13 | 14 | impl TryFrom for Holepunch { 15 | type Error = Error; 16 | 17 | fn try_from(value: ExtendedMessage) -> Result { 18 | if value.0 != Self::ID { 19 | return Err(crate::error::Error::WrongExtensionId { 20 | local: Self::ID, 21 | received: value.0, 22 | }); 23 | } 24 | 25 | let _holepunch = Holepunch::deserialize(&value.1)?; 26 | 27 | todo!(); 28 | } 29 | } 30 | 31 | impl ExtMsgHandler for Peer { 32 | type Error = Error; 33 | 34 | async fn handle_msg(&mut self, msg: Holepunch) -> Result<(), Error> { 35 | let Some(_remote_ext_id) = self.state.extension.as_ref() else { 36 | return Ok(()); 37 | }; 38 | 39 | match msg.msg_type { 40 | HolepunchMsgType::Rendezvous => { 41 | // 1. check if we have the target peer. 42 | // 2. send Connect msg to the src peer. 43 | // 3. send Connect msg to the target peer. 44 | 45 | let (otx, orx) = oneshot::channel(); 46 | 47 | self.state 48 | .ctx 49 | .torrent_ctx 50 | .tx 51 | .send(TorrentMsg::ReadPeerByIp( 52 | msg.addr.into(), 53 | msg.port, 54 | otx, 55 | )) 56 | .await?; 57 | 58 | // 59 | // send connect to the src 60 | // 61 | let Some(_peer_ctx) = orx.await? else { 62 | // peer.state 63 | // .sink 64 | // .send( 65 | // ExtendedMessage( 66 | // remote_ext_id, 67 | // msg.error(HolepunchErrorCodes::NotConnected) 68 | // .try_into()?, 69 | // ) 70 | // .into(), 71 | // ) 72 | // .await?; 73 | return Ok(()); 74 | }; 75 | 76 | // if the peer doesn't support the holepunch protocol. 77 | // if peer.state.ext_states.holepunch.is_none() { 78 | // peer.state 79 | // .sink 80 | // .send( 81 | // ExtendedMessage( 82 | // remote_ext_id, 83 | // msg.error(HolepunchErrorCodes::NoSupport) 84 | // .try_into()?, 85 | // ) 86 | // .into(), 87 | // ) 88 | // .await?; 89 | // return Ok(()); 90 | // } 91 | 92 | // peer.state 93 | // .sink 94 | // .send( 95 | // ExtendedMessage( 96 | // remote_ext_id, 97 | // Holepunch::connect( 98 | // peer_ctx.remote_addr.into(), 99 | // peer_ctx.remote_addr.port(), 100 | // ) 101 | // .try_into()?, 102 | // ) 103 | // .into(), 104 | // ) 105 | // .await?; 106 | // 107 | // send connect to the target 108 | // 109 | // peer.state 110 | // .sink 111 | // .send( 112 | // ExtendedMessage( 113 | // remote_ext_id, 114 | // Holepunch::connect( 115 | // peer.state.ctx.remote_addr.into(), 116 | // peer.state.ctx.remote_addr.port(), 117 | // ) 118 | // .try_into()?, 119 | // ) 120 | // .into(), 121 | // ) 122 | // .await?; 123 | } 124 | HolepunchMsgType::Connect => { 125 | // try to do handshake 126 | } 127 | HolepunchMsgType::Error => { 128 | // 129 | } 130 | } 131 | 132 | Ok(()) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use std::{ 3 | ops::{Deref, DerefMut}, 4 | time::Duration, 5 | }; 6 | 7 | use crossterm::event::EventStream; 8 | use futures::{FutureExt, StreamExt}; 9 | use ratatui::{ 10 | backend::CrosstermBackend as Backend, 11 | crossterm::{ 12 | self, cursor, 13 | event::{EnableBracketedPaste, EnableMouseCapture}, 14 | terminal::EnterAlternateScreen, 15 | }, 16 | }; 17 | use tokio::{ 18 | sync::mpsc::{self, Receiver, Sender}, 19 | task::JoinHandle, 20 | }; 21 | use tokio_util::sync::CancellationToken; 22 | 23 | #[derive(Clone, Debug)] 24 | pub enum Event { 25 | TerminalEvent(ratatui::crossterm::event::Event), 26 | Tick, 27 | Render, 28 | Quit, 29 | Error, 30 | } 31 | 32 | pub struct Tui { 33 | pub terminal: ratatui::Terminal>, 34 | pub task: JoinHandle<()>, 35 | pub cancellation_token: CancellationToken, 36 | pub event_rx: Receiver, 37 | pub event_tx: Sender, 38 | pub mouse: bool, 39 | pub paste: bool, 40 | } 41 | 42 | impl Tui { 43 | const FRAME_RATE: f64 = 60.0; 44 | const TICK_RATE: f64 = 4.0; 45 | 46 | pub fn new() -> Result { 47 | let terminal = ratatui::init(); 48 | let (event_tx, event_rx) = mpsc::channel(50); 49 | let cancellation_token = CancellationToken::new(); 50 | let task = tokio::spawn(async {}); 51 | let mouse = false; 52 | let paste = false; 53 | 54 | Ok(Self { 55 | terminal, 56 | task, 57 | cancellation_token, 58 | event_rx, 59 | event_tx, 60 | mouse, 61 | paste, 62 | }) 63 | } 64 | 65 | pub fn mouse(mut self, mouse: bool) -> Self { 66 | self.mouse = mouse; 67 | self 68 | } 69 | 70 | pub fn paste(mut self, paste: bool) -> Self { 71 | self.paste = paste; 72 | self 73 | } 74 | 75 | fn start(&mut self) { 76 | let tick_delay = Duration::from_secs_f64(1.0 / Self::TICK_RATE); 77 | let render_delay = Duration::from_secs_f64(1.0 / Self::FRAME_RATE); 78 | 79 | self.cancellation_token = CancellationToken::new(); 80 | 81 | let cancellation_token = self.cancellation_token.clone(); 82 | let event_tx = self.event_tx.clone(); 83 | 84 | tokio::spawn(async move { 85 | let mut reader = EventStream::default(); 86 | let mut tick_interval = tokio::time::interval(tick_delay); 87 | let mut render_interval = tokio::time::interval(render_delay); 88 | 89 | loop { 90 | let tick_delay = tick_interval.tick(); 91 | let render_delay = render_interval.tick(); 92 | 93 | tokio::select! { 94 | event = reader.next().fuse() => { 95 | if let Some(Ok(event)) = event { 96 | event_tx.send(Event::TerminalEvent(event)).await?; 97 | } 98 | }, 99 | _ = tick_delay => { 100 | event_tx.send(Event::Tick).await?; 101 | }, 102 | _ = render_delay => { 103 | event_tx.send(Event::Render).await?; 104 | }, 105 | _ = cancellation_token.cancelled() => { 106 | break; 107 | } 108 | } 109 | } 110 | Ok::<(), Error>(()) 111 | }); 112 | } 113 | 114 | pub fn run(&mut self) -> Result<(), Error> { 115 | crossterm::terminal::enable_raw_mode().unwrap(); 116 | crossterm::execute!( 117 | std::io::stderr(), 118 | EnterAlternateScreen, 119 | cursor::Hide 120 | ) 121 | .unwrap(); 122 | if self.mouse { 123 | crossterm::execute!(std::io::stderr(), EnableMouseCapture).unwrap(); 124 | } 125 | if self.paste { 126 | crossterm::execute!(std::io::stderr(), EnableBracketedPaste) 127 | .unwrap(); 128 | } 129 | self.start(); 130 | Ok(()) 131 | } 132 | 133 | pub fn cancel(&self) { 134 | self.cancellation_token.cancel(); 135 | ratatui::restore(); 136 | } 137 | 138 | pub async fn next(&mut self) -> Result { 139 | self.event_rx.recv().await.ok_or(Error::RecvError) 140 | } 141 | } 142 | 143 | impl Deref for Tui { 144 | type Target = ratatui::Terminal>; 145 | 146 | fn deref(&self) -> &Self::Target { 147 | &self.terminal 148 | } 149 | } 150 | 151 | impl DerefMut for Tui { 152 | fn deref_mut(&mut self) -> &mut Self::Target { 153 | &mut self.terminal 154 | } 155 | } 156 | 157 | impl Drop for Tui { 158 | fn drop(&mut self) { 159 | self.cancel(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | daemon::DaemonMsg, disk::DiskMsg, peer::PeerMsg, torrent::TorrentMsg, 3 | }; 4 | use std::{io, path::PathBuf}; 5 | use thiserror::Error; 6 | use tokio::{ 7 | sync::{mpsc, oneshot}, 8 | task::JoinError, 9 | }; 10 | 11 | impl From for Error { 12 | fn from(_value: bendy::decoding::Error) -> Self { 13 | Self::BencodeError 14 | } 15 | } 16 | 17 | impl From for Error { 18 | fn from(_value: bendy::encoding::Error) -> Self { 19 | Self::BencodeError 20 | } 21 | } 22 | 23 | impl From> for Error { 24 | fn from(value: mpsc::error::SendError) -> Self { 25 | Self::SendDaemonError(value.to_string()) 26 | } 27 | } 28 | 29 | impl From> for Error { 30 | fn from(_value: mpsc::error::SendError) -> Self { 31 | Self::SendErrorDisk 32 | } 33 | } 34 | 35 | impl From for Error { 36 | fn from(value: std::convert::Infallible) -> Self { 37 | match value {} 38 | } 39 | } 40 | 41 | #[derive(Error, Debug)] 42 | pub enum Error { 43 | #[error( 44 | "Received wrong extension ID: `{received}` different from the local: \ 45 | `{received}`" 46 | )] 47 | WrongExtensionId { local: u8, received: u8 }, 48 | 49 | #[error("File not found: {0}")] 50 | FileNotFound(PathBuf), 51 | 52 | #[error("")] 53 | FrontendError, 54 | 55 | #[error("Join error: {0}")] 56 | JoinError(#[from] JoinError), 57 | 58 | #[error("Rkyv error: {0}")] 59 | Rkyv(#[from] rkyv::rancor::Error), 60 | 61 | #[error("Failed to send a connect request to the tracker")] 62 | MagnetError(#[from] magnet_url::MagnetError), 63 | 64 | #[error("String is not UTF-8")] 65 | Utf8Error(#[from] std::string::FromUtf8Error), 66 | 67 | #[error("Failed to encode")] 68 | BincodeEncodeError(#[from] bincode::error::EncodeError), 69 | 70 | #[error("Failed to dencode")] 71 | BincodeDecodeError(#[from] bincode::error::DecodeError), 72 | 73 | #[error("Failed to decode or encode the bencode buffer")] 74 | BencodeError, 75 | 76 | #[error("IO error")] 77 | IO(#[from] io::Error), 78 | 79 | #[error("Tracker resolved to no unusable addresses")] 80 | TrackerNoHosts, 81 | 82 | #[error("Peer resolved to no unusable addresses")] 83 | TrackerSocketAddr, 84 | 85 | #[error("The response received from the connect handshake was wrong")] 86 | TrackerResponse, 87 | 88 | #[error("Received a buffer with an unexpected length.")] 89 | ResponseLen, 90 | 91 | #[error( 92 | "Can't parse compact ip list because it's not divisible by the ip \ 93 | version." 94 | )] 95 | CompactPeerListRemainder, 96 | 97 | #[error("Could not connect to the UDP socket of the tracker")] 98 | TrackerSocketConnect, 99 | 100 | #[error("Error while trying to load configuration: `{0}")] 101 | FromConfigError(#[from] config::ConfigError), 102 | 103 | #[error("Error when reading magnet link")] 104 | MagnetLinkInvalid, 105 | 106 | #[error("The response received from the peer is wrong")] 107 | MessageResponse, 108 | 109 | #[error("The handshake received is not valid")] 110 | HandshakeInvalid, 111 | 112 | #[error("The peer took to long to send the handshake")] 113 | HandshakeTimeout, 114 | 115 | #[error("The peer didn't send a handshake as the first message")] 116 | NoHandshake, 117 | 118 | #[error( 119 | "Could not open the file `{0}`. Please make sure the program has \ 120 | permission to access it" 121 | )] 122 | FileOpenError(String), 123 | 124 | #[error("This torrent is already downloaded fully")] 125 | TorrentComplete, 126 | 127 | #[error("Could not find torrent for the given info_hash")] 128 | TorrentDoesNotExist, 129 | 130 | #[error("The piece downloaded does not have a valid hash")] 131 | PieceInvalid, 132 | 133 | #[error("The peer closed the socket")] 134 | PeerClosedSocket, 135 | 136 | #[error( 137 | "Your magnet doesn't have any UDP tracker, for now only those are \ 138 | supported." 139 | )] 140 | NoUDPTracker, 141 | 142 | #[error("Could not send message to Disk")] 143 | SendErrorDisk, 144 | 145 | #[error("Could not send message to Daemon: {0}")] 146 | SendDaemonError(String), 147 | 148 | #[error("Could not receive message from oneshot")] 149 | ReceiveErrorOneshot(#[from] oneshot::error::RecvError), 150 | 151 | #[error("Could not send message to Peer")] 152 | SendErrorPeer(#[from] mpsc::error::SendError), 153 | 154 | #[error("Could not send message to UI")] 155 | SendErrorTorrent(#[from] mpsc::error::SendError), 156 | 157 | #[error("Could not send message to TCP socket")] 158 | SendErrorTcp, 159 | 160 | #[error("You cannot add a duplicate torrent, only 1 is allowed")] 161 | NoDuplicateTorrent, 162 | 163 | #[error("Can't have duplicate peers")] 164 | NoDuplicatePeer, 165 | 166 | #[error("No peers in the torrent")] 167 | NoPeers, 168 | } 169 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(if_let_guard)] 2 | 3 | use crossterm::event::{ 4 | Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, 5 | MouseEventKind, 6 | }; 7 | use ratatui::layout::{Constraint, Flex, Layout, Rect}; 8 | 9 | pub mod action; 10 | pub mod app; 11 | pub mod error; 12 | pub mod pages; 13 | pub mod palette; 14 | pub mod tui; 15 | pub mod widgets; 16 | pub use palette::*; 17 | 18 | /// Return a floating centered Rect 19 | fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { 20 | let vertical = Layout::vertical([Constraint::Percentage(percent_y)]) 21 | .flex(Flex::Center); 22 | let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]) 23 | .flex(Flex::Center); 24 | let [area] = vertical.areas(r); 25 | let [area] = horizontal.areas(area); 26 | area 27 | } 28 | 29 | #[derive(Debug, Clone, Default, PartialEq, Hash, Eq)] 30 | pub struct Input { 31 | /// Typed key. 32 | pub key: Key, 33 | /// Ctrl modifier key. `true` means Ctrl key was pressed. 34 | pub ctrl: bool, 35 | /// Alt modifier key. `true` means Alt key was pressed. 36 | pub alt: bool, 37 | /// Shift modifier key. `true` means Shift key was pressed. 38 | pub shift: bool, 39 | } 40 | 41 | #[derive(Clone, Default, Copy, Debug, PartialEq, Hash, Eq)] 42 | pub enum Key { 43 | /// Normal letter key input 44 | Char(char), 45 | /// F1, F2, F3, ... keys 46 | F(u8), 47 | /// Backspace key 48 | Backspace, 49 | /// Enter or return key 50 | Enter, 51 | /// Left arrow key 52 | Left, 53 | /// Right arrow key 54 | Right, 55 | /// Up arrow key 56 | Up, 57 | /// Down arrow key 58 | Down, 59 | /// Tab key 60 | Tab, 61 | /// Delete key 62 | Delete, 63 | /// Home key 64 | Home, 65 | /// End key 66 | End, 67 | /// Page up key 68 | PageUp, 69 | /// Page down key 70 | PageDown, 71 | /// Escape key 72 | Esc, 73 | /// Copy key. This key is supported by termwiz only 74 | Copy, 75 | /// Cut key. This key is supported by termwiz only 76 | Cut, 77 | /// Paste key. This key is supported by termwiz only 78 | Paste, 79 | /// Virtual key to scroll down by mouse 80 | MouseScrollDown, 81 | /// Virtual key to scroll up by mouse 82 | MouseScrollUp, 83 | /// An invalid key input 84 | #[default] 85 | Null, 86 | } 87 | 88 | impl From for Input { 89 | /// Convert [`crossterm::event::Event`] into [`Input`]. 90 | fn from(event: Event) -> Self { 91 | match event { 92 | Event::Key(key) => Self::from(key), 93 | Event::Mouse(mouse) => Self::from(mouse), 94 | _ => Self::default(), 95 | } 96 | } 97 | } 98 | 99 | impl From for Key { 100 | /// Convert [`crossterm::event::KeyCode`] into [`Key`]. 101 | fn from(code: KeyCode) -> Self { 102 | match code { 103 | KeyCode::Char(c) => Key::Char(c), 104 | KeyCode::Backspace => Key::Backspace, 105 | KeyCode::Enter => Key::Enter, 106 | KeyCode::Left => Key::Left, 107 | KeyCode::Right => Key::Right, 108 | KeyCode::Up => Key::Up, 109 | KeyCode::Down => Key::Down, 110 | KeyCode::Tab => Key::Tab, 111 | KeyCode::Delete => Key::Delete, 112 | KeyCode::Home => Key::Home, 113 | KeyCode::End => Key::End, 114 | KeyCode::PageUp => Key::PageUp, 115 | KeyCode::PageDown => Key::PageDown, 116 | KeyCode::Esc => Key::Esc, 117 | KeyCode::F(x) => Key::F(x), 118 | _ => Key::Null, 119 | } 120 | } 121 | } 122 | 123 | impl From for Input { 124 | /// Convert [`crossterm::event::KeyEvent`] into [`Input`]. 125 | fn from(key: KeyEvent) -> Self { 126 | if key.kind == KeyEventKind::Release { 127 | // On Windows or when 128 | // `crossterm::event::PushKeyboardEnhancementFlags` is set, 129 | // key release event can be reported. Ignore it. (#14) 130 | return Self::default(); 131 | } 132 | 133 | let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); 134 | let alt = key.modifiers.contains(KeyModifiers::ALT); 135 | let shift = key.modifiers.contains(KeyModifiers::SHIFT); 136 | let key = Key::from(key.code); 137 | 138 | Self { key, ctrl, alt, shift } 139 | } 140 | } 141 | 142 | impl From for Key { 143 | /// Convert [`crossterm::event::MouseEventKind`] into [`Key`]. 144 | fn from(kind: MouseEventKind) -> Self { 145 | match kind { 146 | MouseEventKind::ScrollDown => Key::MouseScrollDown, 147 | MouseEventKind::ScrollUp => Key::MouseScrollUp, 148 | _ => Key::Null, 149 | } 150 | } 151 | } 152 | 153 | impl From for Input { 154 | /// Convert [`crossterm::event::MouseEvent`] into [`Input`]. 155 | fn from(mouse: MouseEvent) -> Self { 156 | let key = Key::from(mouse.kind); 157 | let ctrl = mouse.modifiers.contains(KeyModifiers::CONTROL); 158 | let alt = mouse.modifiers.contains(KeyModifiers::ALT); 159 | let shift = mouse.modifiers.contains(KeyModifiers::SHIFT); 160 | Self { key, ctrl, alt, shift } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /crates/vcz-lib/src/tracker/connect.rs: -------------------------------------------------------------------------------- 1 | use super::action::Action; 2 | use crate::error::Error; 3 | use rkyv::{ 4 | Archive, Deserialize, Serialize, api::high::to_bytes_with_alloc, 5 | ser::allocator::Arena, util::AlignedVec, 6 | }; 7 | 8 | #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Archive)] 9 | #[rkyv(compare(PartialEq), derive(Debug))] 10 | pub struct Request { 11 | pub protocol_id: u64, 12 | pub action: Action, 13 | pub transaction_id: u32, 14 | } 15 | 16 | impl Default for Request { 17 | fn default() -> Self { 18 | Self::new() 19 | } 20 | } 21 | 22 | impl Request { 23 | pub const LEN: usize = 16; 24 | const MAGIC: u64 = 0x41727101980; 25 | const MAGIC_BUF: [u8; 8] = Self::MAGIC.to_be_bytes(); 26 | 27 | pub fn new() -> Self { 28 | Self { 29 | protocol_id: Self::MAGIC, 30 | action: Action::Connect, 31 | transaction_id: rand::random::(), 32 | } 33 | } 34 | 35 | /// Zero-copy serialize, this takes around `484ns` in release mode while 36 | /// serializing by hand takes around `1.94µs`, around 4x faster. 37 | pub fn serialize(&self) -> Result { 38 | let mut arena = Arena::with_capacity(16); 39 | Ok(to_bytes_with_alloc::<_, rkyv::rancor::Error>( 40 | self, 41 | arena.acquire(), 42 | )?) 43 | } 44 | 45 | #[cfg(test)] 46 | fn serialize_hand(&self) -> [u8; 16] { 47 | let mut buf = [0u8; 16]; 48 | buf[..8].copy_from_slice(&Self::MAGIC.to_be_bytes()); 49 | buf[8..12].copy_from_slice(&(self.action as u32).to_be_bytes()); 50 | buf[12..16].copy_from_slice(&self.transaction_id.to_be_bytes()); 51 | buf 52 | } 53 | 54 | pub fn deserialize(bytes: &[u8]) -> Result<&ArchivedRequest, Error> { 55 | if bytes.len() != Self::LEN { 56 | return Err(Error::ResponseLen); 57 | } 58 | if bytes[0..8] != Self::MAGIC_BUF { 59 | return Err(Error::ResponseLen); 60 | } 61 | Ok(unsafe { rkyv::access_unchecked::(bytes) }) 62 | } 63 | } 64 | 65 | #[derive(Debug, PartialEq, Serialize, Deserialize, Archive)] 66 | #[rkyv(compare(PartialEq), derive(Debug))] 67 | pub struct Response { 68 | pub action: Action, 69 | pub transaction_id: u32, 70 | pub connection_id: u64, 71 | } 72 | 73 | impl Response { 74 | pub const LEN: usize = 16; 75 | 76 | #[cfg(test)] 77 | pub(crate) fn hand(buf: &[u8]) -> Result { 78 | if buf.len() != Self::LEN { 79 | return Err(Error::ResponseLen); 80 | } 81 | 82 | // let action = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]); 83 | let action: Action = 84 | u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) 85 | .try_into() 86 | .unwrap(); 87 | let transaction_id = 88 | u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]); 89 | let connection_id = u64::from_be_bytes([ 90 | buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], 91 | buf[15], 92 | ]); 93 | 94 | Ok(Self { action, transaction_id, connection_id }) 95 | } 96 | 97 | pub fn deserialize(bytes: &[u8]) -> Result<&ArchivedResponse, Error> { 98 | if bytes.len() != Self::LEN { 99 | return Err(Error::ResponseLen); 100 | } 101 | Ok(unsafe { rkyv::access_unchecked::(bytes) }) 102 | } 103 | 104 | pub fn serialize(&self) -> Result { 105 | let mut arena = Arena::with_capacity(Self::LEN); 106 | Ok(to_bytes_with_alloc::<_, rkyv::rancor::Error>( 107 | self, 108 | arena.acquire(), 109 | )?) 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use tokio::time::Instant; 116 | 117 | use super::*; 118 | 119 | #[test] 120 | fn new_and_default() { 121 | let n = Request::new(); 122 | assert_eq!(n.action, Action::Connect); 123 | assert_eq!(n.protocol_id, Request::MAGIC); 124 | let d = Request::default(); 125 | assert_eq!(d.action, Action::Connect); 126 | assert_eq!(d.protocol_id, Request::MAGIC); 127 | } 128 | 129 | #[ignore] 130 | #[test] 131 | fn serialize() { 132 | let n = Request::new(); 133 | 134 | let now = Instant::now(); 135 | let b = n.serialize_hand(); 136 | let time = Instant::now().duration_since(now); 137 | println!("hand: {time:?}"); 138 | 139 | let now = Instant::now(); 140 | let c = n.serialize().unwrap(); 141 | let time = Instant::now().duration_since(now); 142 | println!("rkyv: {time:?}"); 143 | 144 | assert_eq!(b, *c); 145 | } 146 | 147 | #[test] 148 | #[ignore] 149 | fn deserialize() { 150 | let n = Response { 151 | action: Action::Scrape, 152 | connection_id: 123, 153 | transaction_id: 987, 154 | }; 155 | let b = n.serialize().unwrap(); 156 | 157 | let now = Instant::now(); 158 | let _ = Response::hand(&b).unwrap(); 159 | let time = Instant::now().duration_since(now); 160 | println!("hand: {time:?}"); 161 | 162 | let now = Instant::now(); 163 | let d = Response::deserialize(&b).unwrap(); 164 | let time = Instant::now().duration_since(now); 165 | println!("rkyv: {time:?}"); 166 | 167 | assert_eq!(n, *d); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Duration}; 2 | 3 | use futures::{SinkExt, Stream, StreamExt}; 4 | use tokio::{ 5 | net::TcpStream, 6 | select, spawn, 7 | sync::mpsc::{UnboundedReceiver, UnboundedSender}, 8 | time::timeout, 9 | }; 10 | use tokio_util::codec::Framed; 11 | use tracing::debug; 12 | use vcz_lib::{ 13 | config::ResolvedConfig, 14 | daemon_wire::{DaemonCodec, Message}, 15 | }; 16 | 17 | use crate::{ 18 | action::Action, 19 | error::Error, 20 | pages::{Page, torrent_list::TorrentList}, 21 | tui::Tui, 22 | }; 23 | 24 | pub struct App { 25 | pub is_detached: bool, 26 | pub tx: UnboundedSender, 27 | pub config: Arc, 28 | should_quit: bool, 29 | page: TorrentList<'static>, 30 | } 31 | 32 | impl App { 33 | pub fn is_detched(mut self, v: bool) -> Self { 34 | self.is_detached = v; 35 | self 36 | } 37 | 38 | pub fn new( 39 | config: Arc, 40 | tx: UnboundedSender, 41 | ) -> Self { 42 | let page = TorrentList::new(tx.clone()); 43 | 44 | App { config, should_quit: false, tx, page, is_detached: false } 45 | } 46 | 47 | pub async fn run( 48 | &mut self, 49 | mut rx: UnboundedReceiver, 50 | ) -> Result<(), Error> { 51 | let mut i = 0; 52 | 53 | let socket = loop { 54 | match timeout( 55 | Duration::from_millis(100), 56 | TcpStream::connect(self.config.daemon_addr), 57 | ) 58 | .await 59 | { 60 | Ok(Ok(v)) => break v, 61 | _ => { 62 | i += 1; 63 | if i > 10 { 64 | return Err(Error::DaemonNotRunning( 65 | self.config.daemon_addr, 66 | )); 67 | } 68 | tokio::time::sleep(Duration::from_millis(150)).await; 69 | } 70 | } 71 | }; 72 | 73 | let mut tui = Tui::new()?; 74 | tui.run()?; 75 | 76 | let tx = self.tx.clone(); 77 | 78 | // spawn event loop to listen to messages sent by the daemon 79 | let socket = Framed::new(socket, DaemonCodec); 80 | let (mut sink, stream) = socket.split(); 81 | let _tx = self.tx.clone(); 82 | 83 | let handle = spawn(async move { 84 | let _ = Self::listen_daemon(_tx, stream).await; 85 | }); 86 | 87 | loop { 88 | let e = tui.next().await?; 89 | let a = self.page.handle_event(e); 90 | let _ = tx.send(a); 91 | 92 | while let Ok(action) = rx.try_recv() { 93 | match action { 94 | Action::Render => { 95 | let _ = tui.draw(|f| { 96 | self.page.draw(f); 97 | }); 98 | } 99 | Action::Quit => { 100 | if !self.is_detached { 101 | let _ = sink.send(Message::Quit).await; 102 | } 103 | handle.abort(); 104 | tui.cancel(); 105 | self.should_quit = true; 106 | } 107 | Action::NewTorrent(magnet) => { 108 | sink.send(Message::NewTorrent(magnet.clone())).await?; 109 | } 110 | Action::DeleteTorrent(info_hash) => { 111 | sink.send(Message::DeleteTorrent(info_hash)).await?; 112 | } 113 | _ => self.page.handle_action(action), 114 | } 115 | } 116 | 117 | if self.should_quit { 118 | sink.send(Message::FrontendQuit).await?; 119 | break; 120 | } 121 | } 122 | 123 | Ok(()) 124 | } 125 | 126 | /// Listen to the messages sent by the daemon via TCP, 127 | /// when we receive a message, we send it to ourselves 128 | /// via mpsc [`Action`]. For example, when we receive 129 | /// a TorrentState message from the daemon, we forward it to ourselves. 130 | pub async fn listen_daemon< 131 | T: Stream> + Unpin, 132 | >( 133 | tx: UnboundedSender, 134 | mut stream: T, 135 | ) { 136 | loop { 137 | select! { 138 | Some(Ok(msg)) = stream.next() => { 139 | match msg { 140 | Message::TorrentState(torrent_state) => { 141 | let _ = tx.send(Action::TorrentState(torrent_state)); 142 | } 143 | Message::TorrentStates(torrent_states) => { 144 | let _ = tx.send(Action::TorrentStates(torrent_states)); 145 | } 146 | Message::Quit => { 147 | debug!("ui Quit"); 148 | let _ = tx.send(Action::Quit); 149 | break; 150 | } 151 | Message::TogglePause(torrent) => { 152 | let _ = tx.send(Action::TogglePause(torrent)); 153 | } 154 | _ => {} 155 | } 156 | } 157 | else => break 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vincenzo 2 | 3 | Vincenzo is a BitTorrent client with vim-like keybindings and a terminal based UI. 4 | 5 | ![image](tape.gif) 6 | 7 | ## About 8 | 9 | Vincenzo aims to be a modern, fast, minimalistic\*, and good-looking BitTorrent client. 10 | 11 | \*Minimalistic here means that the UI is not bloated, configuration is done 12 | through the configuration file, no ads, and no telemetry. 13 | 14 | The official UI runs on the terminal with vim-like keybindings. 15 | 16 | 3 binaries and 1 library: 17 | 18 | - [vcz](crates/vcz) - Main binary with both UI and daemon together. 19 | - [vcz-ui](crates/vcz-ui) - UI binary (connects to daemon remotely). 20 | - [vcz-daemon](crates/vcz-daemon) - Daemon binary. 21 | - [vcz-lib](crates/vcz-lib) - Library. 22 | 23 | > [!WARNING] 24 | > Experimental software moving towards a stable release. 25 | > 26 | > I develop on rust nightly and there is no minimum supported version. 27 | > I have only tested on my x86 Linux machine, but _probably_ works in other platforms. 28 | > 29 | > The protocol is fully implemented with good performance and with many nice 30 | > extensions that you would expect, however, some things are missing and sometimes 31 | > there are crashes. Security features and authentication for remote 32 | > control is not yet implemented. 33 | 34 | ## Features 35 | 36 | - Multi-platform. 37 | - Fast downloads. 38 | - Daemon detached from the UI. Remote control by TCP messages or UI binary in 39 | another machine. (note that this is not secured by authentication yet). 40 | 41 | ## How to use 42 | 43 | Right now, you have to download the repo and compile it from scratch, remember 44 | to use the `--release` flag of cargo. 45 | 46 | ```bash 47 | $ git clone git@github.com:gabrieldemian/vincenzo.git 48 | $ cd vincenzo 49 | $ cargo build --release 50 | $ cd ./target/release 51 | ./vcz 52 | ``` 53 | 54 | ## Configuration 55 | 56 | We have 3 sources of configuration, in order of lowest priority to the highest: 57 | config file -> CLI flags -> ENV variables. 58 | 59 | The config file is located at the default config folder of your OS. 60 | 61 | - Linux: ~/.config/vincenzo/config.toml 62 | - Windows: C:\\Users\\Alice\\AppData\\Roaming\\Vincenzo\\config.toml 63 | - MacOS: /Users/Alice/Library/Application Support/Vincenzo/config.toml 64 | 65 | For the CLI flags, use --help. 66 | 67 | ### CLI flags 68 | 69 | ```text 70 | $vcz --help 71 | 72 | Usage: vczd [OPTIONS] 73 | 74 | Options: 75 | --download-dir 76 | Where to store files of torrents. Defaults to the download dir of the user. 77 | 78 | --metadata-dir 79 | Where to store .torrent files. Defaults to `~/.config/vincenzo/torrents` 80 | 81 | --daemon-addr 82 | Where the daemon listens for connections. Defaults to `0.0.0.0:0` 83 | 84 | --local-peer-port 85 | Port of the client, defaults to 51413 86 | 87 | --max-global-peers 88 | Max number of global TCP connections, defaults to 500 89 | 90 | --max-torrent-peers 91 | Max number of TCP connections for each torrent, defaults to 50, and is 92 | capped by `max_global_peers` 93 | 94 | --is-ipv6 95 | If the client will use an ipv6 socket to connect to other peers. 96 | Defaults to false [possible values: true, false] 97 | 98 | --quit-after-complete 99 | If the daemon should quit after all downloads are complete. Defaults 100 | to false [possible values: true, false] 101 | 102 | -m, --magnet 103 | Add magnet url to the daemon 104 | 105 | -s, --stats 106 | Print the stats of all torrents 107 | 108 | -p, --pause 109 | Pause the torrent with the given info hash 110 | 111 | -q, --quit 112 | Terminate the process of the daemon 113 | 114 | -h, --help 115 | Print help 116 | 117 | -V, --version 118 | Print version 119 | ``` 120 | 121 | ### Default config.toml 122 | 123 | ```toml 124 | download_dir = "$XDG_DOWNLOAD_DIR" 125 | daemon_addr = "0.0.0.0:51411" 126 | max_global_peers = 500 127 | max_torrent_peers = 50 128 | local_peer_port = 51413 129 | is_ipv6 = false 130 | ``` 131 | 132 | ## Supported BEPs 133 | 134 | - [BEP 0003](http://www.bittorrent.org/beps/bep_0003.html) 135 | The BitTorrent Protocol Specification. 136 | 137 | - [BEP 0009](http://www.bittorrent.org/beps/bep_0009.html) 138 | Extension for Peers to Send Metadata Files. 139 | 140 | - [BEP 0010](http://www.bittorrent.org/beps/bep_0010.html) 141 | Extension Protocol. 142 | 143 | - [BEP 0015](http://www.bittorrent.org/beps/bep_0015.html) 144 | UDP Tracker Protocol. 145 | 146 | - [BEP 0023](http://www.bittorrent.org/beps/bep_0023.html) 147 | Tracker Returns Compact Peer Lists. 148 | 149 | ## Roadmap 150 | 151 | | # | Step | Status | 152 | | :-: | --------------------------------------------------------- | :----: | 153 | | 1 | Base core protocol and algorithms: endgame, unchoke, etc. | ✅ | 154 | | 2 | Quality of life extensions: magnet, UDP trackers, etc. | ✅ | 155 | | 3 | Perf: cache, multi-tracker, fast IO, zero-copy, etc. | ✅ | 156 | | 4 | Saving metainfo files on disk after downloading it | ✅ | 157 | | 5 | Select files to download before download starts | ❌ | 158 | | 6 | Streaming of videos | ❌ | 159 | | 7 | µTP, UDP hole punch, and fancy features | ⚠️ | 160 | | ... | Others | ❌ | 161 | -------------------------------------------------------------------------------- /crates/vcz-ui/src/widgets/network_chart.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | Frame, 3 | prelude::*, 4 | widgets::{ 5 | Axis, Block, Borders, Chart, Dataset, GraphType, LegendPosition, 6 | }, 7 | }; 8 | use tokio::time::Instant; 9 | use vcz_lib::{torrent::InfoHash, utils::to_human_readable}; 10 | 11 | use crate::PALETTE; 12 | 13 | /// Last 60 seconds 14 | const MAX_DATA_POINTS: usize = 60; 15 | const TARGET_WINDOW: f64 = 60.0; 16 | const SMOOTHING_FACTOR: f64 = 0.05; // 5% 17 | 18 | #[derive(Clone)] 19 | pub struct NetworkChart { 20 | pub info_hash: InfoHash, 21 | download_data: Vec<(f64, f64)>, 22 | upload_data: Vec<(f64, f64)>, 23 | max_rate: f64, 24 | start_time: Instant, 25 | current_time: f64, 26 | } 27 | 28 | impl Default for NetworkChart { 29 | fn default() -> Self { 30 | Self { 31 | info_hash: InfoHash::default(), 32 | download_data: Vec::with_capacity(MAX_DATA_POINTS), 33 | upload_data: Vec::with_capacity(MAX_DATA_POINTS), 34 | max_rate: 1.0, 35 | start_time: Instant::now(), 36 | current_time: 0.0, 37 | } 38 | } 39 | } 40 | 41 | impl NetworkChart { 42 | pub fn new(info_hash: InfoHash) -> Self { 43 | Self { info_hash, ..Default::default() } 44 | } 45 | 46 | pub fn on_tick(&mut self, download_rate: f64, upload_rate: f64) { 47 | self.current_time = self.start_time.elapsed().as_secs_f64(); 48 | 49 | self.download_data.push((self.current_time, download_rate)); 50 | self.upload_data.push((self.current_time, upload_rate)); 51 | 52 | let min_time = self.current_time - TARGET_WINDOW; 53 | self.download_data.retain(|&(t, _)| t >= min_time); 54 | self.upload_data.retain(|&(t, _)| t >= min_time); 55 | 56 | // calculate the maximum of both datasets using log scale 57 | let current_max_log = self 58 | .download_data 59 | .iter() 60 | .chain(self.upload_data.iter()) 61 | .map(|&(_, rate)| (rate + 1.0).log10()) // add 1 to avoid log(0) 62 | .fold(0.0, f64::max); 63 | 64 | // if we have a new maximum, update immediately (no smoothing) 65 | if self.max_rate < current_max_log { 66 | self.max_rate = current_max_log * 1.1; 67 | } else { 68 | self.max_rate = SMOOTHING_FACTOR * current_max_log 69 | + (1.0 - SMOOTHING_FACTOR) * self.max_rate; 70 | } 71 | 72 | self.max_rate = self.max_rate.max(0.1); 73 | } 74 | 75 | pub fn draw(&self, frame: &mut Frame, area: Rect, dim: bool) { 76 | if self.download_data.is_empty() || self.upload_data.is_empty() { 77 | return; 78 | } 79 | 80 | let min_time = (self.current_time - TARGET_WINDOW).max(0.0); 81 | let max_time = min_time + TARGET_WINDOW; 82 | 83 | let log_download_data: Vec<(f64, f64)> = self 84 | .download_data 85 | .iter() 86 | .map(|&(t, rate)| (t, (rate + 1.0).log10())) 87 | .collect(); 88 | 89 | let log_upload_data: Vec<(f64, f64)> = self 90 | .upload_data 91 | .iter() 92 | .map(|&(t, rate)| (t, (rate + 1.0).log10())) 93 | .collect(); 94 | 95 | let download_dataset = Dataset::default() 96 | .name("") 97 | .marker(symbols::Marker::Braille) 98 | .graph_type(GraphType::Line) 99 | .style(PALETTE.blue) 100 | .data(&log_download_data); 101 | 102 | let upload_dataset = Dataset::default() 103 | .name("") 104 | .marker(symbols::Marker::Braille) 105 | .graph_type(GraphType::Line) 106 | .style(PALETTE.green) 107 | .data(&log_upload_data); 108 | 109 | let x_labels = vec![ 110 | Span::from("0s").bold(), 111 | Span::from("30s").bold(), 112 | Span::from("60s").bold(), 113 | ]; 114 | 115 | let y_ticks = [ 116 | 0.0, // 10^0 = 1 B/s 117 | self.max_rate * 0.25, // 25% of max 118 | self.max_rate * 0.5, // 50% of max 119 | self.max_rate * 0.75, // 75% of max 120 | self.max_rate, // 100% of max 121 | ]; 122 | 123 | let y_labels: Vec = y_ticks 124 | .iter() 125 | .map(|&log_value| { 126 | let value = 10_f64.powf(log_value); 127 | Span::styled( 128 | to_human_readable(value), 129 | Style::default().add_modifier(Modifier::BOLD), 130 | ) 131 | }) 132 | .collect(); 133 | 134 | let legend = vec![ 135 | Span::styled(" ↓ ", PALETTE.blue), 136 | Span::styled( 137 | to_human_readable(self.download_data.last().unwrap().1), 138 | Into::