caesura 0.20.0

An all-in-one command line tool to **transcode FLAC** audio files and **upload to gazelle** based indexers/trackers.
Documentation
use crate::errors::AppError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::fmt::{Display, Formatter};

const HEXADECIMAL_RADIX: u32 = 16;

/// Byte array hash
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Hash<const N: usize> {
    bytes: [u8; N],
}

impl<const N: usize> Hash<N> {
    /// Create a new hash from a byte array
    pub fn new(bytes: [u8; N]) -> Self {
        Self { bytes }
    }

    /// Creates a hash from a hexadecimal string.
    pub fn from_string(hex: &str) -> Result<Self, AppError> {
        let bytes = to_bytes(hex)?;
        Ok(Hash::new(bytes))
    }

    /// Get the hash as a hexadecimal string.
    pub fn to_hex(&self) -> String {
        self.bytes.iter().fold(String::new(), |mut acc, &b| {
            acc.push_str(&format!("{b:02x}"));
            acc
        })
    }

    /// Get the byte array
    pub fn as_bytes(&self) -> &[u8; N] {
        &self.bytes
    }

    /// Truncate the hash
    ///
    /// Returns `None` if `M` > `N`
    pub fn truncate<const M: usize>(&self) -> Option<Hash<M>> {
        let bytes: [u8; M] = self.bytes[..M].try_into().ok()?;
        Some(Hash::new(bytes))
    }
}

impl<const N: usize> Default for Hash<N> {
    fn default() -> Self {
        Self::new([0; N])
    }
}

impl<const N: usize> Display for Hash<N> {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.to_hex())
    }
}

impl<const N: usize> Serialize for Hash<N> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_hex())
    }
}

impl<'de, const N: usize> Deserialize<'de> for Hash<N> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let hex_str = String::deserialize(deserializer)?;
        #[allow(clippy::absolute_paths)]
        Hash::from_string(&hex_str).map_err(serde::de::Error::custom)
    }
}

/// Convert a hexadecimal string to a 20-byte array.
#[allow(clippy::needless_range_loop)]
#[allow(clippy::indexing_slicing)]
fn to_bytes<const N: usize>(hex: &str) -> Result<[u8; N], AppError> {
    let length = hex.len();
    if length != N * 2 {
        return AppError::explained(
            "convert hash",
            format!("Length was not {}: {length}", N * 2),
        );
    }
    let mut bytes = [0_u8; N];
    for i in 0..N {
        let start = i * 2;
        let byte_str = &hex[start..start + 2];
        bytes[i] = to_byte(byte_str)?;
    }
    Ok(bytes)
}

/// Convert a 2-character hexadecimal string to a byte.
fn to_byte(hex: &str) -> Result<u8, AppError> {
    u8::from_str_radix(hex, HEXADECIMAL_RADIX).map_err(|_| {
        AppError::else_explained("convert hash", format!("Invalid hex character: {hex}"))
    })
}