Skip to main content

pith_digest/
digest.rs

1//! The fixed-width digest value and the byte source it is read from.
2
3use core::fmt;
4use core::fmt::Write as _;
5
6use crate::error::{Error, Result};
7
8/// The lowercase hex alphabet [`Digest::to_hex_into`] writes.
9const HEX: &[u8; 16] = b"0123456789abcdef";
10
11/// One-character hex digits for [`fmt::Display`], so the streaming
12/// formatter never needs a `2 * N` buffer.
13const HEX_DIGIT: [&str; 16] = [
14    "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f",
15];
16
17/// A fixed-width hash value of `N` bytes.
18///
19/// `N` is the output width in bytes. The value is copied around by
20/// value: it is small, and hex output in this workspace is compared
21/// byte-for-byte against specification vectors constantly, so hex
22/// encoding never allocates — [`to_hex_into`](Digest::to_hex_into)
23/// writes into a caller-owned fixed buffer, not a heap string.
24///
25/// [`Default`] is the all-zero value and is *not* a valid hash of
26/// anything: a zeroed buffer is what a failed hash computation tends to
27/// leave behind, and it must not be mistaken for a result.
28///
29/// Ordering is bytewise lexicographic on the raw bytes, matching
30/// `[u8; N]` itself, so digests can serve as index keys.
31#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct Digest<const N: usize> {
33    /// The raw bytes of the digest.
34    bytes: [u8; N],
35}
36
37// Manual, not derived: `Default` for `[T; N]` only exists up to
38// `N <= 32`, and a 64-byte SHA-512 digest must still default to zero.
39impl<const N: usize> Default for Digest<N> {
40    /// The all-zero digest. Not a valid hash of anything; see the type
41    /// documentation.
42    fn default() -> Self {
43        Digest { bytes: [0u8; N] }
44    }
45}
46
47impl<const N: usize> Digest<N> {
48    /// The number of characters in the hex form: exactly `2 * N`.
49    pub const HEX_LEN: usize = 2 * N;
50
51    /// Wraps `N` bytes. Never fails.
52    ///
53    /// ```
54    /// let d = pith_digest::Digest::<2>::from_bytes([0xaa, 0x55]);
55    /// assert_eq!(d.as_bytes(), &[0xaa, 0x55]);
56    /// ```
57    pub fn from_bytes(bytes: [u8; N]) -> Self {
58        Digest { bytes }
59    }
60
61    /// Borrows the raw `N` bytes.
62    pub fn as_bytes(&self) -> &[u8; N] {
63        &self.bytes
64    }
65
66    /// Writes the lowercase hexadecimal form into `out`.
67    ///
68    /// `out` must be exactly [`HEX_LEN`](Self::HEX_LEN) bytes; any other
69    /// length is [`Error::BadValue`](`crate::Error::BadValue`) `"hex
70    /// buffer length"`. No allocation, no `hex` crate, and leading
71    /// zeroes are kept — the output is exactly `2 * N` characters.
72    ///
73    /// The buffer is caller-owned rather than returned because stable
74    /// Rust cannot name the type `[u8; 2 * N]` inside a generic
75    /// context (that needs `generic_const_exprs`, still unstable at the
76    /// workspace MSRV); at a concrete `N` the caller's array is a plain
77    /// constant-sized type.
78    ///
79    /// ```
80    /// let d = pith_digest::Digest::<2>::from_bytes([0x00, 0xff]);
81    /// let mut hex = [0u8; 4];
82    /// d.to_hex_into(&mut hex).unwrap();
83    /// assert_eq!(&hex[..], b"00ff");
84    /// ```
85    pub fn to_hex_into(self, out: &mut [u8]) -> Result<()> {
86        if out.len() != Self::HEX_LEN {
87            return Err(Error::BadValue("hex buffer length"));
88        }
89        for (i, &byte) in self.bytes.iter().enumerate() {
90            out[2 * i] = HEX[usize::from(byte >> 4)];
91            out[2 * i + 1] = HEX[usize::from(byte & 0x0f)];
92        }
93        Ok(())
94    }
95
96    /// Parses exactly `2 * N` hexadecimal characters, lowercase or
97    /// uppercase.
98    ///
99    /// Rejections are named: an odd-length input is
100    /// [`Error::BadValue`](`crate::Error::BadValue`) `"hex length is
101    /// odd"`, an even input shorter than `2 * N` is
102    /// [`Error::Truncated`](`crate::Error::Truncated`), an input longer
103    /// than `2 * N` is [`Error::BadValue`](`crate::Error::BadValue`)
104    /// `"hex digest length"`, and any non-hex byte is
105    /// [`Error::BadValue`](`crate::Error::BadValue`) `"hex digit"`.
106    ///
107    /// ```
108    /// let d = pith_digest::Digest::<4>::from_hex(b"DeadBeef").unwrap();
109    /// assert_eq!(d.as_bytes(), &[0xde, 0xad, 0xbe, 0xef]);
110    /// assert_eq!(
111    ///     pith_digest::Digest::<4>::from_hex(b"odd"),
112    ///     Err(pith_digest::Error::BadValue("hex length is odd"))
113    /// );
114    /// ```
115    pub fn from_hex(hex: &[u8]) -> Result<Self> {
116        if hex.len() % 2 != 0 {
117            return Err(Error::BadValue("hex length is odd"));
118        }
119        if hex.len() < 2 * N {
120            return Err(Error::truncated("hex digest", 2 * N, hex.len()));
121        }
122        if hex.len() > 2 * N {
123            return Err(Error::BadValue("hex digest length"));
124        }
125        let mut bytes = [0u8; N];
126        for (dst, pair) in bytes.iter_mut().zip(hex.chunks_exact(2)) {
127            *dst = (nibble(pair[0])? << 4) | nibble(pair[1])?;
128        }
129        Ok(Digest { bytes })
130    }
131
132    /// Copies exactly `N` bytes out of `data`.
133    ///
134    /// A shorter slice is [`Error::Truncated`](`crate::Error::Truncated`)
135    /// with `needed` set to `N`; a longer one is
136    /// [`Error::BadValue`](`crate::Error::BadValue`) `"digest length"`.
137    pub fn from_slice(data: &[u8]) -> Result<Self> {
138        if data.len() < N {
139            return Err(Error::truncated("digest", N, data.len()));
140        }
141        if data.len() > N {
142            return Err(Error::BadValue("digest length"));
143        }
144        let mut bytes = [0u8; N];
145        bytes.copy_from_slice(data);
146        Ok(Digest { bytes })
147    }
148
149    /// Reads exactly `N` bytes out of `stream`.
150    ///
151    /// Partial reads are followed until the digest is full. A source
152    /// that supplies fewer than `N` bytes before it is exhausted yields
153    /// [`Error::Truncated`](`crate::Error::Truncated`) with `found` set
154    /// to the number of bytes actually supplied. A source with more than
155    /// `N` bytes simply keeps the rest.
156    ///
157    /// ```
158    /// let mut src: &[u8] = &[1, 2, 3];
159    /// let d = pith_digest::Digest::<3>::from_stream(&mut src).unwrap();
160    /// assert_eq!(d.as_bytes(), &[1, 2, 3]);
161    /// ```
162    pub fn from_stream<R: Read + ?Sized>(stream: &mut R) -> Result<Self> {
163        let mut bytes = [0u8; N];
164        let mut filled = 0usize;
165        while filled < N {
166            let n = stream.read_some(&mut bytes[filled..]);
167            if n == 0 {
168                return Err(Error::truncated("digest", N, filled));
169            }
170            filled += n;
171        }
172        Ok(Digest { bytes })
173    }
174}
175
176impl<const N: usize> fmt::Display for Digest<N> {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        // Streams the same digits `to_hex_into` writes, one character
179        // pair per byte, so the two paths cannot drift apart. Fill,
180        // alignment and width are honoured by hand: a generic `[u8;
181        // 2 * N]` buffer cannot exist here (see `to_hex_into`).
182        let pad = f.width().map_or(0, |w| w.saturating_sub(2 * N));
183        let fill = f.fill();
184        let (before, after) = match f.align().unwrap_or(fmt::Alignment::Left) {
185            fmt::Alignment::Left => (0, pad),
186            fmt::Alignment::Right => (pad, 0),
187            fmt::Alignment::Center => (pad / 2, pad - pad / 2),
188        };
189        for _ in 0..before {
190            f.write_char(fill)?;
191        }
192        for &byte in self.bytes.iter() {
193            f.write_str(HEX_DIGIT[usize::from(byte >> 4)])?;
194            f.write_str(HEX_DIGIT[usize::from(byte & 0x0f)])?;
195        }
196        for _ in 0..after {
197            f.write_char(fill)?;
198        }
199        Ok(())
200    }
201}
202
203/// Maps one ASCII byte to its nibble value.
204fn nibble(byte: u8) -> Result<u8> {
205    match byte {
206        b'0'..=b'9' => Ok(byte - b'0'),
207        b'a'..=b'f' => Ok(byte - b'a' + 10),
208        b'A'..=b'F' => Ok(byte - b'A' + 10),
209        _ => Err(Error::BadValue("hex digit")),
210    }
211}
212
213/// The minimal byte source [`Digest::from_stream`] reads from.
214///
215/// The kit is `no_std` and `core`-only, so it cannot name
216/// `std::io::Read`; this trait carries the one operation a digest
217/// needs: *pull some bytes now, returning how many*. Host code adapts a
218/// real reader in one line, e.g. for `std::io::Read`:
219/// `fn read_some(&mut self, buf: &mut [u8]) -> usize { self.0.read(buf).unwrap_or(0) }`
220/// — an I/O error surfaces as exhaustion, which `from_stream` reports
221/// as [`Error::Truncated`](`crate::Error::Truncated`).
222pub trait Read {
223    /// Pulls as many bytes as the source can supply right now into
224    /// `buf` and returns how many were written. Never writes more than
225    /// `buf.len()`. Returning `0` signals that the source is exhausted.
226    fn read_some(&mut self, buf: &mut [u8]) -> usize;
227}
228
229impl Read for &[u8] {
230    fn read_some(&mut self, buf: &mut [u8]) -> usize {
231        let n = self.len().min(buf.len());
232        buf[..n].copy_from_slice(&self[..n]);
233        *self = &self[n..];
234        n
235    }
236}