zlib-header 0.1.2

Library to work with the 2 Byte zlib header, as defined in RFC 1950.
Documentation
//! Library to work with the 2 Byte zlib header, as defined in
//! [RFC 1950](https://datatracker.ietf.org/doc/html/rfc1950).  
//! # Examples
//! ```
//! use zlib_header::ZlibHeader;
//! let cm = 8;
//! let cinfo = 7;
//! let fdict = false;
//! let flevel = 2;
//! let header = ZlibHeader::new(cm, cinfo, fdict, flevel);
//! match header {
//!   Ok(header) => {
//!     println!("header is valid (strict): {}", header.is_valid_strict()); // header is valid (strict): true 
//!   },
//!   Err(err) => eprintln!("Unable to initialize zlib header: {:?}", err)
//! }
//! ```
//! ```
//! use zlib_header::ZlibHeader;
//! let header = ZlibHeader::default();
//! println!("Display: {}", header); // Display: 789C
//! println!("Debug: {:?}", header); // Debug: ZlibHeader { DEFLATE | 32768 Bytes | default | Dictionary: false | valid }
//! let bytes = [0x78, 0x9C];
//! println!("header matches expected bytes: {}", header == bytes); // header matches expected bytes: true
//! ```
use hex::FromHexError;
use std::fmt::{Debug, Display, Formatter};

/// The error type when a value does not fit inside the possible range of a certain number of bits.  
#[derive(Debug)]
pub enum OutOfRangeError {
    CompressionMethod(String),
    CompressionInfo(String),
    CompressionLevel(String),
}

/// 2 Byte header at the start of a zlib stream.
#[repr(C)]
pub struct ZlibHeader {
    /// Compression Method and Flags  
    /// 0000_1111 => compression method (`cm`)  
    /// 1111_0000 => compression info (`cinfo`)  
    pub cmf: u8,
    /// Flags  
    /// 0001_1111 => checksum adjustment (`fcheck`)  
    /// 0010_0000 => preset dictionary used (`fdict`)  
    /// 1100_0000 => compression level (`flevel`)  
    pub flg: u8,
}

impl ZlibHeader {
    /// Initializes with the given parameters and calls [`Self::set_fcheck`].  
    pub fn new(cm: u8, cinfo: u8, fdict: bool, flevel: u8) -> Result<Self, OutOfRangeError> {
        let mut header = Self { cmf: 0, flg: 0 };
        header.set_cm(cm)?;
        header.set_cinfo(cinfo)?;
        header.set_fdict(fdict);
        header.set_flevel(flevel)?;
        header.set_fcheck::<false>();
        Ok(header)
    }

    /// Parses `input` as 2 Byte hex string to initialize.  
    /// # Errors  
    /// [`FromHexError::InvalidStringLength`] if `input` is not exactly 4 characters long.  
    /// Other [`FromHexError`] variants if [`hex::decode`] fails by other means.  
    pub fn from_hex(input: &str) -> Result<Self, FromHexError> {
        if input.len() != 4 {
            return Err(FromHexError::InvalidStringLength);
        }
        let bytes = hex::decode(input)?;
        let header = Self {
            cmf: bytes[0],
            flg: bytes[1],
        };
        Ok(header)
    }

    /// Gets the lower 4 bits of `self.cmf`, representing the compression method.  
    pub fn get_cm(&self) -> u8 {
        self.cmf & 0b0000_1111
    }

    /// Sets the lower 4 bits of `self.cmf` to `cm`, representing the compression method.  
    /// # Errors  
    /// Returns [`OutOfRangeError::CompressionInfo`] if `cm > 15`  
    pub fn set_cm(&mut self, cm: u8) -> Result<(), OutOfRangeError> {
        if cm > 15 {
            let msg = format!("cm was {}, but must be between 0 and 15", cm);
            return Err(OutOfRangeError::CompressionInfo(msg));
        }
        self.cmf = (self.cmf & 0b1111_0000) | cm;
        Ok(())
    }

    /// Gets the string representation of `cm`.  
    /// `DEFLATE` if it is 8, `UNDEFINED` in all other cases.  
    pub fn get_cm_str(&self) -> &str {
        match self.get_cm() {
            8 => "DEFLATE",
            _ => "UNDEFINED",
        }
    }

    /// Gets the upper 4 bits of `self.cmf`, representing the compression info.  
    /// It is used to determine the sliding window size for de-/compression.  
    /// Read more on [`Self::get_window_size`]  
    pub fn get_cinfo(&self) -> u8 {
        self.cmf >> 4
    }

    /// Sets the upper 4 bits of `self.cmf` to `cinfo`, representing the compression info.  
    /// # Errors  
    /// Returns [`OutOfRangeError::CompressionInfo`] if `cinfo > 15`  
    pub fn set_cinfo(&mut self, cinfo: u8) -> Result<(), OutOfRangeError> {
        if cinfo > 15 {
            let msg = format!("cinfo was {}, but must be between 0 and 15", cinfo);
            return Err(OutOfRangeError::CompressionInfo(msg));
        }
        self.cmf = (self.cmf & 0b0000_1111) | cinfo << 4;
        Ok(())
    }

    /// Gets the size of the sliding window in Bytes.  
    /// Valid window sizes range from 256 to 32768 - means `cinfo` ranges from 0 to 7.  
    /// The formula is: `2.pow(cinfo + 8)`  
    pub fn get_window_size(&self) -> u32 {
        2u32.pow(self.get_cinfo() as u32 + 8)
    }

    /// Gets the lowest 5 bits of `self.flg`, representing the checksum adjustment.  
    /// The value is chosen to satisfy the checksum formula over the entire `ZlibHeader`.  
    /// Read more on [`Self::is_valid`]  
    pub fn get_fcheck(&self) -> u8 {
        self.flg & 0b0001_1111
    }

    /// Sets the lowest 5 bits of `self.flg`, representing the checksum adjustment.  
    /// The value is chosen to satisfy the checksum formula over the entire `ZlibHeader`.  
    /// The generic constant `CLEAN` dictates if the function has to zero the current `fcheck` bits.  
    /// Read more on [`Self::is_valid`]  
    pub fn set_fcheck<const CLEAN: bool>(&mut self) {
        let clean_flg = match CLEAN {
            false => self.flg,
            true => self.flg & 0b1110_0000,
        };
        let clean_header: u16 = ((self.cmf as u16) << 8) + clean_flg as u16;
        let fcheck = 31 - clean_header % 31;
        self.flg = clean_flg | fcheck as u8;
    }

    /// Returns `true` if the checksum formula over the `ZlibHeader` is satisfied.  
    /// The formula is: `(self.cmf * 256 + self.flg) % 31 == 0`  
    pub fn is_valid(&self) -> bool {
        (self.cmf as usize * 256 + self.flg as usize) % 31 == 0
    }

    /// In addition to [`Self::is_valid`] it also checks `cm == 8` and `cinfo <= 7`.
    pub fn is_valid_strict(&self) -> bool {
        let is_valid = self.is_valid();
        let is_deflate = self.get_cm() == 8;
        let cinfo = self.get_cinfo();
        let is_valid_cinfo = cinfo <= 7;
        is_valid && is_deflate && is_valid_cinfo
    }

    /// Gets the bit at index 5 of `self.flg` as `bool`, signaling the usage of a preset dictionary.
    pub fn get_fdict(&self) -> bool {
        (self.flg >> 5) & 1 == 1
    }

    /// Sets the bit at index 5 of `self.flg`, signaling the usage of a preset dictionary.  
    pub fn set_fdict(&mut self, fdict: bool) {
        let mask: u8 = 0b1101_1111;
        self.flg = (self.flg & mask) | if fdict { !mask } else { 0 };
    }

    /// Returns the upper 2 bits of `self.flg`, which represents the compression level.  
    pub fn get_flevel(&self) -> u8 {
        self.flg >> 6
    }

    /// Sets the upper 2 bits of `self.flg` to `flevel`, which represents the compression level.  
    /// # Errors  
    /// Returns [`OutOfRangeError::CompressionLevel`] if `flevel > 3`  
    pub fn set_flevel(&mut self, flevel: u8) -> Result<(), OutOfRangeError> {
        if flevel > 3 {
            let msg = format!("flevel was {}, but must be between 0 and 3", flevel);
            return Err(OutOfRangeError::CompressionInfo(msg));
        }
        self.flg = (self.flg & 0b0011_1111) | (flevel << 6);
        Ok(())
    }

    /// Gets the string representation of `flevel`.  
    /// The values are: `fastest`, `fast`, `default`, `best`  
    pub fn get_flevel_str(&self) -> &str {
        match self.get_flevel() {
            0 => "fastest",
            1 => "fast",
            2 => "default",
            3 => "best",
            _ => unreachable!("only has 2 bits"),
        }
    }
}

impl PartialEq<[u8; 2]> for ZlibHeader {
    fn eq(&self, slice: &[u8; 2]) -> bool {
        self.cmf == slice[0] && self.flg == slice[1]
    }
}

impl PartialEq<ZlibHeader> for [u8; 2] {
    fn eq(&self, header: &ZlibHeader) -> bool {
        self[0] == header.cmf && self[1] == header.flg
    }
}

impl PartialEq<&[u8]> for ZlibHeader {
    fn eq(&self, slice: &&[u8]) -> bool {
        if slice.len() != 2 {
            panic!("u8 slice must be exactly 2 Bytes when comparing ZlibHeader");
        }
        self.cmf == slice[0] && self.flg == slice[1]
    }
}

impl PartialEq<ZlibHeader> for &[u8] {
    fn eq(&self, header: &ZlibHeader) -> bool {
        if self.len() != 2 {
            panic!("u8 slice must be exactly 2 Bytes when comparing ZlibHeader");
        }
        self[0] == header.cmf && self[1] == header.flg
    }
}

impl From<[u8; 2]> for ZlibHeader {
    fn from(bytes: [u8; 2]) -> Self {
        Self {
            cmf: bytes[0],
            flg: bytes[1],
        }
    }
}

impl From<ZlibHeader> for [u8; 2] {
    fn from(header: ZlibHeader) -> Self {
        [header.cmf, header.flg]
    }
}

impl Default for ZlibHeader {
    /// `789C` - this is DEFLATE with default compression level and a 32 KiB window.
    fn default() -> Self {
        Self {
            cmf: 0x78,
            flg: 0x9C,
        }
    }
}

impl Display for ZlibHeader {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(&format!("{:02X}{:02X}", self.cmf, self.flg))
    }
}

impl Debug for ZlibHeader {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let cm = self.get_cm_str();
        let window_size = self.get_window_size();
        let flevel = self.get_flevel_str();
        let fdict = self.get_fdict();
        let validity = if self.is_valid() { "valid" } else { "invalid" };
        let str = &format!(
            "ZlibHeader {{ {} | {} Bytes | {} | Dictionary: {} | {} }}",
            cm, window_size, flevel, fdict, validity
        );
        f.write_str(str)
    }
}