zlib_header/
lib.rs

1//! Library to work with the 2 Byte zlib header, as defined in
2//! [RFC 1950](https://datatracker.ietf.org/doc/html/rfc1950).  
3//! # Examples
4//! ```
5//! use zlib_header::ZlibHeader;
6//! let cm = 8;
7//! let cinfo = 7;
8//! let fdict = false;
9//! let flevel = 2;
10//! let header = ZlibHeader::new(cm, cinfo, fdict, flevel);
11//! match header {
12//!   Ok(header) => {
13//!     println!("header is valid (strict): {}", header.is_valid_strict()); // header is valid (strict): true 
14//!   },
15//!   Err(err) => eprintln!("Unable to initialize zlib header: {:?}", err)
16//! }
17//! ```
18//! ```
19//! use zlib_header::ZlibHeader;
20//! let header = ZlibHeader::default();
21//! println!("Display: {}", header); // Display: 789C
22//! println!("Debug: {:?}", header); // Debug: ZlibHeader { DEFLATE | 32768 Bytes | default | Dictionary: false | valid }
23//! let bytes = [0x78, 0x9C];
24//! println!("header matches expected bytes: {}", header == bytes); // header matches expected bytes: true
25//! ```
26use hex::FromHexError;
27use std::fmt::{Debug, Display, Formatter};
28
29/// The error type when a value does not fit inside the possible range of a certain number of bits.  
30#[derive(Debug)]
31pub enum OutOfRangeError {
32    CompressionMethod(String),
33    CompressionInfo(String),
34    CompressionLevel(String),
35}
36
37/// 2 Byte header at the start of a zlib stream.
38#[repr(C)]
39pub struct ZlibHeader {
40    /// Compression Method and Flags  
41    /// 0000_1111 => compression method (`cm`)  
42    /// 1111_0000 => compression info (`cinfo`)  
43    pub cmf: u8,
44    /// Flags  
45    /// 0001_1111 => checksum adjustment (`fcheck`)  
46    /// 0010_0000 => preset dictionary used (`fdict`)  
47    /// 1100_0000 => compression level (`flevel`)  
48    pub flg: u8,
49}
50
51impl ZlibHeader {
52    /// Initializes with the given parameters and calls [`Self::set_fcheck`].  
53    pub fn new(cm: u8, cinfo: u8, fdict: bool, flevel: u8) -> Result<Self, OutOfRangeError> {
54        let mut header = Self { cmf: 0, flg: 0 };
55        header.set_cm(cm)?;
56        header.set_cinfo(cinfo)?;
57        header.set_fdict(fdict);
58        header.set_flevel(flevel)?;
59        header.set_fcheck::<false>();
60        Ok(header)
61    }
62
63    /// Parses `input` as 2 Byte hex string to initialize.  
64    /// # Errors  
65    /// [`FromHexError::InvalidStringLength`] if `input` is not exactly 4 characters long.  
66    /// Other [`FromHexError`] variants if [`hex::decode`] fails by other means.  
67    pub fn from_hex(input: &str) -> Result<Self, FromHexError> {
68        if input.len() != 4 {
69            return Err(FromHexError::InvalidStringLength);
70        }
71        let bytes = hex::decode(input)?;
72        let header = Self {
73            cmf: bytes[0],
74            flg: bytes[1],
75        };
76        Ok(header)
77    }
78
79    /// Gets the lower 4 bits of `self.cmf`, representing the compression method.  
80    pub fn get_cm(&self) -> u8 {
81        self.cmf & 0b0000_1111
82    }
83
84    /// Sets the lower 4 bits of `self.cmf` to `cm`, representing the compression method.  
85    /// # Errors  
86    /// Returns [`OutOfRangeError::CompressionInfo`] if `cm > 15`  
87    pub fn set_cm(&mut self, cm: u8) -> Result<(), OutOfRangeError> {
88        if cm > 15 {
89            let msg = format!("cm was {}, but must be between 0 and 15", cm);
90            return Err(OutOfRangeError::CompressionInfo(msg));
91        }
92        self.cmf = (self.cmf & 0b1111_0000) | cm;
93        Ok(())
94    }
95
96    /// Gets the string representation of `cm`.  
97    /// `DEFLATE` if it is 8, `UNDEFINED` in all other cases.  
98    pub fn get_cm_str(&self) -> &str {
99        match self.get_cm() {
100            8 => "DEFLATE",
101            _ => "UNDEFINED",
102        }
103    }
104
105    /// Gets the upper 4 bits of `self.cmf`, representing the compression info.  
106    /// It is used to determine the sliding window size for de-/compression.  
107    /// Read more on [`Self::get_window_size`]  
108    pub fn get_cinfo(&self) -> u8 {
109        self.cmf >> 4
110    }
111
112    /// Sets the upper 4 bits of `self.cmf` to `cinfo`, representing the compression info.  
113    /// # Errors  
114    /// Returns [`OutOfRangeError::CompressionInfo`] if `cinfo > 15`  
115    pub fn set_cinfo(&mut self, cinfo: u8) -> Result<(), OutOfRangeError> {
116        if cinfo > 15 {
117            let msg = format!("cinfo was {}, but must be between 0 and 15", cinfo);
118            return Err(OutOfRangeError::CompressionInfo(msg));
119        }
120        self.cmf = (self.cmf & 0b0000_1111) | cinfo << 4;
121        Ok(())
122    }
123
124    /// Gets the size of the sliding window in Bytes.  
125    /// Valid window sizes range from 256 to 32768 - means `cinfo` ranges from 0 to 7.  
126    /// The formula is: `2.pow(cinfo + 8)`  
127    pub fn get_window_size(&self) -> u32 {
128        2u32.pow(self.get_cinfo() as u32 + 8)
129    }
130
131    /// Gets the lowest 5 bits of `self.flg`, representing the checksum adjustment.  
132    /// The value is chosen to satisfy the checksum formula over the entire `ZlibHeader`.  
133    /// Read more on [`Self::is_valid`]  
134    pub fn get_fcheck(&self) -> u8 {
135        self.flg & 0b0001_1111
136    }
137
138    /// Sets the lowest 5 bits of `self.flg`, representing the checksum adjustment.  
139    /// The value is chosen to satisfy the checksum formula over the entire `ZlibHeader`.  
140    /// The generic constant `CLEAN` dictates if the function has to zero the current `fcheck` bits.  
141    /// Read more on [`Self::is_valid`]  
142    pub fn set_fcheck<const CLEAN: bool>(&mut self) {
143        let clean_flg = match CLEAN {
144            false => self.flg,
145            true => self.flg & 0b1110_0000,
146        };
147        let clean_header: u16 = ((self.cmf as u16) << 8) + clean_flg as u16;
148        let fcheck = 31 - clean_header % 31;
149        self.flg = clean_flg | fcheck as u8;
150    }
151
152    /// Returns `true` if the checksum formula over the `ZlibHeader` is satisfied.  
153    /// The formula is: `(self.cmf * 256 + self.flg) % 31 == 0`  
154    pub fn is_valid(&self) -> bool {
155        (self.cmf as usize * 256 + self.flg as usize) % 31 == 0
156    }
157
158    /// In addition to [`Self::is_valid`] it also checks `cm == 8` and `cinfo <= 7`.
159    pub fn is_valid_strict(&self) -> bool {
160        let is_valid = self.is_valid();
161        let is_deflate = self.get_cm() == 8;
162        let cinfo = self.get_cinfo();
163        let is_valid_cinfo = cinfo <= 7;
164        is_valid && is_deflate && is_valid_cinfo
165    }
166
167    /// Gets the bit at index 5 of `self.flg` as `bool`, signaling the usage of a preset dictionary.
168    pub fn get_fdict(&self) -> bool {
169        (self.flg >> 5) & 1 == 1
170    }
171
172    /// Sets the bit at index 5 of `self.flg`, signaling the usage of a preset dictionary.  
173    pub fn set_fdict(&mut self, fdict: bool) {
174        let mask: u8 = 0b1101_1111;
175        self.flg = (self.flg & mask) | if fdict { !mask } else { 0 };
176    }
177
178    /// Returns the upper 2 bits of `self.flg`, which represents the compression level.  
179    pub fn get_flevel(&self) -> u8 {
180        self.flg >> 6
181    }
182
183    /// Sets the upper 2 bits of `self.flg` to `flevel`, which represents the compression level.  
184    /// # Errors  
185    /// Returns [`OutOfRangeError::CompressionLevel`] if `flevel > 3`  
186    pub fn set_flevel(&mut self, flevel: u8) -> Result<(), OutOfRangeError> {
187        if flevel > 3 {
188            let msg = format!("flevel was {}, but must be between 0 and 3", flevel);
189            return Err(OutOfRangeError::CompressionInfo(msg));
190        }
191        self.flg = (self.flg & 0b0011_1111) | (flevel << 6);
192        Ok(())
193    }
194
195    /// Gets the string representation of `flevel`.  
196    /// The values are: `fastest`, `fast`, `default`, `best`  
197    pub fn get_flevel_str(&self) -> &str {
198        match self.get_flevel() {
199            0 => "fastest",
200            1 => "fast",
201            2 => "default",
202            3 => "best",
203            _ => unreachable!("only has 2 bits"),
204        }
205    }
206}
207
208impl PartialEq<[u8; 2]> for ZlibHeader {
209    fn eq(&self, slice: &[u8; 2]) -> bool {
210        self.cmf == slice[0] && self.flg == slice[1]
211    }
212}
213
214impl PartialEq<ZlibHeader> for [u8; 2] {
215    fn eq(&self, header: &ZlibHeader) -> bool {
216        self[0] == header.cmf && self[1] == header.flg
217    }
218}
219
220impl PartialEq<&[u8]> for ZlibHeader {
221    fn eq(&self, slice: &&[u8]) -> bool {
222        if slice.len() != 2 {
223            panic!("u8 slice must be exactly 2 Bytes when comparing ZlibHeader");
224        }
225        self.cmf == slice[0] && self.flg == slice[1]
226    }
227}
228
229impl PartialEq<ZlibHeader> for &[u8] {
230    fn eq(&self, header: &ZlibHeader) -> bool {
231        if self.len() != 2 {
232            panic!("u8 slice must be exactly 2 Bytes when comparing ZlibHeader");
233        }
234        self[0] == header.cmf && self[1] == header.flg
235    }
236}
237
238impl From<[u8; 2]> for ZlibHeader {
239    fn from(bytes: [u8; 2]) -> Self {
240        Self {
241            cmf: bytes[0],
242            flg: bytes[1],
243        }
244    }
245}
246
247impl From<ZlibHeader> for [u8; 2] {
248    fn from(header: ZlibHeader) -> Self {
249        [header.cmf, header.flg]
250    }
251}
252
253impl Default for ZlibHeader {
254    /// `789C` - this is DEFLATE with default compression level and a 32 KiB window.
255    fn default() -> Self {
256        Self {
257            cmf: 0x78,
258            flg: 0x9C,
259        }
260    }
261}
262
263impl Display for ZlibHeader {
264    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
265        f.write_str(&format!("{:02X}{:02X}", self.cmf, self.flg))
266    }
267}
268
269impl Debug for ZlibHeader {
270    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
271        let cm = self.get_cm_str();
272        let window_size = self.get_window_size();
273        let flevel = self.get_flevel_str();
274        let fdict = self.get_fdict();
275        let validity = if self.is_valid() { "valid" } else { "invalid" };
276        let str = &format!(
277            "ZlibHeader {{ {} | {} Bytes | {} | Dictionary: {} | {} }}",
278            cm, window_size, flevel, fdict, validity
279        );
280        f.write_str(str)
281    }
282}