Skip to main content

cuttlefish_host/
hex.rs

1//! Lowercase hex for digest bytes.
2//!
3//! Exists because `sha2` 0.11 (via `digest` 0.11 and `hybrid-array`) returns
4//! a plain `Array` from `finalize`/`digest`, and that type no longer
5//! implements `LowerHex` the way `GenericArray` did in 0.10. Every
6//! `format!("{:x}", ...)` on a hash therefore stopped compiling.
7//!
8//! One helper rather than an inline `fold` at each of the four call sites:
9//! these strings are content-addressing keys and graph fingerprints that get
10//! compared against values already written to disk, so all four must agree
11//! on the encoding exactly. A single function makes that agreement
12//! structural instead of a thing four separate lines have to keep getting
13//! right.
14//!
15//! Deliberately not a new dependency. The whole job is two lines, and a
16//! digest is the only thing this codebase ever hex-encodes.
17
18/// Encode `bytes` as lowercase hex, two characters per byte.
19///
20/// Byte-for-byte identical to what `format!("{:x}", digest)` produced under
21/// `sha2` 0.10, which matters: existing blob filenames, catalog index
22/// entries, and recorded graph fingerprints were all written with the old
23/// formatting, and a change here would silently invalidate every one of
24/// them.
25pub fn encode(bytes: impl AsRef<[u8]>) -> String {
26    use std::fmt::Write as _;
27    bytes.as_ref().iter().fold(String::new(), |mut out, b| {
28        // Infallible: writing to a String never fails.
29        let _ = write!(out, "{b:02x}");
30        out
31    })
32}
33
34#[cfg(test)]
35mod tests {
36    use super::encode;
37
38    #[test]
39    fn it_matches_the_formatting_sha2_0_10_produced() {
40        // The well-known SHA-256 of the empty input, in the exact lowercase,
41        // zero-padded, unseparated form the old `{:x}` produced. If this
42        // ever drifts, every catalog blob name and graph fingerprint already
43        // on disk stops matching.
44        use sha2::{Digest, Sha256};
45        assert_eq!(
46            encode(Sha256::digest(b"")),
47            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
48        );
49    }
50
51    #[test]
52    fn a_leading_zero_byte_keeps_both_of_its_characters() {
53        // The failure mode a naive `{:x}` per byte would have: `0x0a`
54        // rendering as "a" rather than "0a", which silently shortens the
55        // string and collides distinct digests.
56        assert_eq!(encode([0x00, 0x0a, 0xff]), "000aff");
57    }
58}