btpeer 0.9.0

Simple CLI tool and library to get peers from TCP/HTTP and UDP BitTorrent trackers
Documentation
use anyhow::{Context, Result, bail};
use clap::{Args, Parser, Subcommand};
use url::Url;

#[derive(Subcommand, Debug)]
pub enum Command {
    #[command(name = "announce")]
    Announce(Announce),
    #[command(name = "scrape")]
    Scrape(Scrape),
}

#[derive(Parser, Debug)]
#[command(name = "btpeer", version, about = "Simple CLI tool and library to get peers from TCP/HTTP and UDP BitTorrent trackers", long_about = None)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Args, Debug)]
#[command(version, about, long_about = None)]
pub struct Announce {
    /// UDP or TCP/HTTP tracker URL
    #[arg(short, long)]
    pub tracker: Vec<Url>,
    /// Socket read timeout
    #[arg(short = 'T', long, default_value_t = 15)]
    pub timeout: u64,
    /// Info-hash subject
    #[arg(short, long, value_parser = info_hash_v1)]
    pub info_hash: [u8; 20],
    /// Bind port for outgoing connections
    /// * set zero to append fake peers (auto)
    #[arg(short, long, default_value_t = 6881)]
    pub port: u16,
    /// Expected buffer size for peer entries
    #[arg(short = 'B', long, default_value_t = 100)]
    pub peers_buffer_capacity: usize,
    /// Proxy URL for HTTP trackers (e.g. I2P)
    #[arg(short = 'P', long)]
    pub proxy_url: Option<String>,
}

#[derive(Args, Debug)]
#[command(version, about, long_about = None)]
pub struct Scrape {
    /// UDP or TCP/HTTP tracker URL
    #[arg(short, long)]
    pub tracker: Vec<Url>,
    /// Socket read timeout
    #[arg(short = 'T', long, default_value_t = 15)]
    pub timeout: u64,
    /// Info-hash subject
    #[arg(short, long, value_parser = info_hash_v1)]
    pub info_hash: Vec<[u8; 20]>,
    /// Bind port for outgoing connections
    /// * set zero to append fake peers (auto)
    #[arg(short, long, default_value_t = 6881)]
    pub port: u16,
    /// Expected buffer size for info-hash entries
    #[arg(short = 'B', long, default_value_t = 100)]
    pub info_hash_buffer_capacity: usize,
    /// Proxy URL for HTTP trackers (e.g. I2P)
    #[arg(short = 'P', long)]
    pub proxy_url: Option<String>,
}

fn info_hash_v1(s: &str) -> Result<[u8; 20]> {
    if s.len() != 40 {
        bail!("Info-hash v1 must be exactly 40 hex characters long");
    }

    let mut bytes = [0u8; 20];

    for (i, b) in bytes.iter_mut().enumerate() {
        let start = i * 2;
        *b = u8::from_str_radix(&s[start..start + 2], 16)
            .with_context(|| format!("Invalid hex character at index {start}"))?;
    }

    Ok(bytes)
}