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}