Skip to main content

pg_embedded_setup_unpriv/extensions/
digest.rs

1//! SHA-256 digests as lower-case hex, validated at the boundary.
2
3use std::{fmt, io};
4
5use sha2::{Digest, Sha256};
6
7/// A SHA-256 digest rendered as 64 lower-case hex characters.
8///
9/// # Examples
10///
11/// ```
12/// use pg_embedded_setup_unpriv::extensions::Sha256Hex;
13///
14/// let digest = Sha256Hex::of_bytes(b"hello");
15/// assert_eq!(
16///     digest.as_str(),
17///     "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
18/// );
19/// assert!(Sha256Hex::parse("2CF24DBA").is_err());
20/// ```
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct Sha256Hex(String);
23
24/// Why a digest string was rejected.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct InvalidDigest {
27    /// The offending value, truncated for messages.
28    pub value: String,
29}
30
31impl fmt::Display for InvalidDigest {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(
34            f,
35            "digest {:?} must be 64 lower-case hex characters",
36            self.value
37        )
38    }
39}
40
41impl std::error::Error for InvalidDigest {}
42
43impl Sha256Hex {
44    /// Parses a digest, requiring exactly 64 lower-case hex characters.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`InvalidDigest`] for any other input, including upper-case
49    /// hex, so a pin is always written in one canonical form.
50    pub fn parse(value: &str) -> Result<Self, InvalidDigest> {
51        let is_valid = value.len() == 64
52            && value
53                .bytes()
54                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte));
55        if is_valid {
56            Ok(Self(value.to_owned()))
57        } else {
58            Err(InvalidDigest {
59                value: value.chars().take(80).collect(),
60            })
61        }
62    }
63
64    /// Computes the digest of an in-memory byte slice.
65    #[must_use]
66    pub fn of_bytes(bytes: &[u8]) -> Self { Self::from_hasher(Sha256::new_with_prefix(bytes)) }
67
68    /// Computes the digest of everything read from `reader`.
69    ///
70    /// # Errors
71    ///
72    /// Returns the underlying I/O error when reading fails.
73    pub fn of_reader<R: io::Read>(mut reader: R) -> io::Result<Self> {
74        let mut hasher = Sha256::new();
75        let mut buffer = vec![0_u8; 64 * 1024];
76        loop {
77            let count = reader.read(&mut buffer)?;
78            if count == 0 {
79                break;
80            }
81            hasher.update(buffer.get(..count).unwrap_or_default());
82        }
83        Ok(Self::from_hasher(hasher))
84    }
85
86    /// Computes the digest of the file at `path`.
87    ///
88    /// # Errors
89    ///
90    /// Returns the underlying I/O error when the file cannot be read.
91    pub fn of_file(path: &camino::Utf8Path) -> io::Result<Self> {
92        Self::of_reader(std::fs::File::open(path)?)
93    }
94
95    /// Renders a finished hasher as lower-case hexadecimal.
96    fn from_hasher(hasher: Sha256) -> Self {
97        let bytes = hasher.finalize();
98        let mut encoded = String::with_capacity(bytes.len() * 2);
99        for byte in bytes {
100            encoded.push(lower_hex_digit(byte >> 4));
101            encoded.push(lower_hex_digit(byte & 0x0f));
102        }
103        Self(encoded)
104    }
105
106    /// Returns the hex digest.
107    #[must_use]
108    pub fn as_str(&self) -> &str { &self.0 }
109}
110
111impl AsRef<str> for Sha256Hex {
112    fn as_ref(&self) -> &str { &self.0 }
113}
114
115impl fmt::Display for Sha256Hex {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) }
117}
118
119/// Renders one nibble as a lower-case hex digit.
120const fn lower_hex_digit(nibble: u8) -> char {
121    if nibble < 10 {
122        (b'0' + nibble) as char
123    } else {
124        (b'a' + nibble - 10) as char
125    }
126}
127
128/// Writer adaptor that hashes everything written through it.
129pub(super) struct HashingWriter<W> {
130    inner: W,
131    hasher: Sha256,
132    written: u64,
133}
134
135impl<W: io::Write> HashingWriter<W> {
136    /// Wraps `inner` so every byte written through it is hashed.
137    pub(super) fn new(inner: W) -> Self {
138        Self {
139            inner,
140            hasher: Sha256::new(),
141            written: 0,
142        }
143    }
144
145    /// Finishes hashing and returns the inner writer, the digest and the byte count.
146    pub(super) fn finish(self) -> (W, Sha256Hex, u64) {
147        (
148            self.inner,
149            Sha256Hex::from_hasher(self.hasher),
150            self.written,
151        )
152    }
153}
154
155impl<W: io::Write> io::Write for HashingWriter<W> {
156    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
157        let count = self.inner.write(buf)?;
158        self.hasher.update(buf.get(..count).unwrap_or_default());
159        self.written += count as u64;
160        Ok(count)
161    }
162
163    fn flush(&mut self) -> io::Result<()> { self.inner.flush() }
164}