torrust-actix 4.1.2

A rich, fast and efficient Bittorrent Tracker.
//! Core utility functions used throughout the tracker.
//!
//! This module provides common helper functions for query parsing, hex encoding,
//! logging setup, time utilities, and graceful shutdown handling.

use crate::common::structs::custom_error::CustomError;
use crate::config::structs::configuration::Configuration;
use async_std::future;
use fern::colors::{Color, ColoredLevelConfig};
use log::info;
use smallvec::SmallVec;
use std::collections::HashMap;
use std::fmt;
use std::fmt::Formatter;
use std::time::{Duration, SystemTime};
use tokio_shutdown::Shutdown;

/// Type alias for query parameter values (optimized for single values).
///
/// Uses `SmallVec` to avoid heap allocation for the common case of
/// a single value per parameter.
pub type QueryValues = SmallVec<[Vec<u8>; 1]>;

/// Parses a URL query string into a HashMap of parameter names to values.
///
/// Handles URL percent-encoding and supports multiple values per parameter
/// (e.g., multiple `info_hash` parameters in scrape requests).
///
/// # Arguments
///
/// * `query` - Optional query string (without the leading `?`)
///
/// # Returns
///
/// A HashMap mapping lowercase parameter names to their decoded byte values.
///
/// # Example
///
/// ```rust,ignore
/// use torrust_actix::common::common::parse_query;
///
/// let params = parse_query(Some("info_hash=%ab%cd...&peer_id=%12%34...".to_string()))?;
/// let info_hash = params.get("info_hash").and_then(|v| v.first());
/// ```
#[inline]
pub fn parse_query(query: Option<String>) -> Result<HashMap<String, QueryValues>, CustomError> {
    let mut queries: HashMap<String, QueryValues> = HashMap::with_capacity(12);
    if let Some(result) = query {
        for query_item in result.split('&') {
            if query_item.is_empty() {
                continue;
            }
            if let Some(equal_pos) = query_item.find('=') {
                let (key_part, value_part) = query_item.split_at(equal_pos);
                let key_name_raw = key_part;
                let value_data_raw = &value_part[1..];
                let key_name = if key_name_raw.contains('%') || key_name_raw.contains('+') {
                    percent_encoding::percent_decode_str(key_name_raw)
                        .decode_utf8_lossy()
                        .to_lowercase()
                } else {
                    key_name_raw.to_ascii_lowercase()
                };
                if key_name.is_empty() {
                    continue;
                }
                let value_data = percent_encoding::percent_decode_str(value_data_raw).collect::<Vec<u8>>();
                queries
                    .entry(key_name)
                    .or_default()
                    .push(value_data);
            } else {
                let key_name = if query_item.contains('%') || query_item.contains('+') {
                    percent_encoding::percent_decode_str(query_item)
                        .decode_utf8_lossy()
                        .to_lowercase()
                } else {
                    query_item.to_ascii_lowercase()
                };
                if key_name.is_empty() {
                    continue;
                }
                queries
                    .entry(key_name)
                    .or_default()
                    .push(Vec::new());
            }
        }
    }
    Ok(queries)
}

pub fn udp_check_host_and_port_used(bind_address: String) {
    if cfg!(target_os = "windows") && let Err(data) = std::net::UdpSocket::bind(&bind_address) {
        sentry::capture_error(&data);
        panic!("Unable to bind to {} ! Exiting...", &bind_address);
    }
}

pub(crate) fn bin2hex(data: &[u8; 20], f: &mut Formatter) -> fmt::Result {
    let mut chars = [0u8; 40];
    binascii::bin2hex(data, &mut chars).expect("failed to hexlify");
    write!(f, "{}", std::str::from_utf8(&chars).unwrap())
}

pub fn hex2bin(data: String) -> Result<[u8; 20], CustomError> {
    hex::decode(data)
        .map_err(|data| {
            sentry::capture_error(&data);
            CustomError::new("error converting hex to bin")
        })
        .and_then(|hash_result| {
            hash_result
                .get(..20)
                .and_then(|slice| slice.try_into().ok())
                .ok_or_else(|| CustomError::new("invalid hex length"))
        })
}

pub fn print_type<T>(_: &T) {
    println!("{:?}", std::any::type_name::<T>());
}

pub fn return_type<T>(_: &T) -> String {
    format!("{:?}", std::any::type_name::<T>())
}

pub fn equal_string_check(source: &str, check: &str) -> bool {
    if source == check {
        return true;
    }
    println!("Source: {source}");
    println!("Check:  {check}");
    false
}

pub fn setup_logging(config: &Configuration) {
    let level = match config.log_level.as_str() {
        "off" => log::LevelFilter::Off,
        "trace" => log::LevelFilter::Trace,
        "debug" => log::LevelFilter::Debug,
        "info" => log::LevelFilter::Info,
        "warn" => log::LevelFilter::Warn,
        "error" => log::LevelFilter::Error,
        _ => {
            panic!("Unknown log level encountered: '{}'", config.log_level.as_str());
        }
    };

    let colors = ColoredLevelConfig::new()
        .trace(Color::Cyan)
        .debug(Color::Magenta)
        .info(Color::Green)
        .warn(Color::Yellow)
        .error(Color::Red);

    fern::Dispatch::new()
        .format(move |out, message, record| {
            out.finish(format_args!(
                "{} [{:width$}][{}] {}",
                chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.9f"),
                colors.color(record.level()),
                record.target(),
                message,
                width = 5
            ))
        })
        .level(level)
        .chain(std::io::stdout())
        .apply()
        .unwrap_or_else(|_| panic!("Failed to initialize logging."));
    info!("logging initialized.");
}

#[inline]
pub fn current_time() -> u64 {
    SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .expect("System time before UNIX epoch")
        .as_secs()
}

#[inline]
pub fn convert_int_to_bytes(number: &u64) -> Vec<u8> {
    let bytes = number.to_be_bytes();
    let leading_zeros = number.leading_zeros() as usize / 8;
    bytes[leading_zeros..].to_vec()
}

#[inline]
pub fn convert_bytes_to_int(array: &[u8]) -> u64 {
    let mut array_fixed = [0u8; 8];
    let len = array.len().min(8);
    array_fixed[8 - len..].copy_from_slice(&array[..len]);
    u64::from_be_bytes(array_fixed)
}

pub async fn shutdown_waiting(timeout: Duration, shutdown_handler: Shutdown) -> bool {
    future::timeout(timeout, shutdown_handler.handle())
        .await
        .is_ok()
}