Skip to main content

cuenv_cas/
digest.rs

1//! Digest type and canonical hashing.
2//!
3//! A [`Digest`] names a blob by `(sha256, size)`. Structurally it mirrors the
4//! Bazel Remote Execution API v2 `Digest` message so that the same value can
5//! later be handed to a `bazel-remote-apis` gRPC client without conversion.
6
7use crate::error::{Error, Result};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest as _, Sha256};
10use std::fmt;
11
12/// A content digest: hex-encoded SHA-256 plus the byte size of the content.
13#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
14pub struct Digest {
15    /// Lowercase hex SHA-256 of the content (64 characters).
16    pub hash: String,
17    /// Length of the content in bytes.
18    pub size_bytes: u64,
19}
20
21impl Digest {
22    /// Compute the digest of `bytes`.
23    #[must_use]
24    pub fn of_bytes(bytes: &[u8]) -> Self {
25        let hash = hex::encode(Sha256::digest(bytes));
26        Self {
27            hash,
28            size_bytes: bytes.len() as u64,
29        }
30    }
31
32    /// Canonical `hash/size` form used in the Bazel RE API resource names.
33    #[must_use]
34    pub fn to_resource(&self) -> String {
35        format!("{}/{}", self.hash, self.size_bytes)
36    }
37}
38
39impl fmt::Display for Digest {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        write!(f, "sha256:{}/{}", self.hash, self.size_bytes)
42    }
43}
44
45/// Serialize a value with stable field ordering for digest computation.
46///
47/// Backed by `serde_json` with `BTreeMap` in the source types — both provide
48/// deterministic ordering, so the output bytes are stable across platforms
49/// and process runs. This is our local pre-protobuf canonical form; when the
50/// remote backend lands we switch to protobuf canonical bytes.
51///
52/// # Errors
53///
54/// Returns [`Error::Serialization`](crate::error::Error::Serialization) if
55/// the value cannot be JSON-encoded.
56pub fn canonical_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>> {
57    serde_json::to_vec(value)
58        .map_err(|e| Error::serialization(format!("canonical encode failed: {e}")))
59}
60
61/// Compute a digest over a serializable value's canonical encoding.
62///
63/// # Errors
64///
65/// Returns any error produced by [`canonical_bytes`].
66pub fn digest_of<T: Serialize>(value: &T) -> Result<Digest> {
67    let bytes = canonical_bytes(value)?;
68    Ok(Digest::of_bytes(&bytes))
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn digest_of_empty_is_stable() {
77        let d = Digest::of_bytes(b"");
78        assert_eq!(d.size_bytes, 0);
79        assert_eq!(
80            d.hash,
81            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
82        );
83    }
84
85    #[test]
86    fn digest_of_hello_world() {
87        let d = Digest::of_bytes(b"hello world");
88        assert_eq!(d.size_bytes, 11);
89        assert_eq!(
90            d.hash,
91            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
92        );
93    }
94
95    #[test]
96    fn display_and_resource_forms() {
97        let d = Digest::of_bytes(b"x");
98        assert!(d.to_string().starts_with("sha256:"));
99        assert!(d.to_resource().contains('/'));
100    }
101
102    #[test]
103    fn digest_of_round_trips_through_serde() {
104        let d = Digest::of_bytes(b"payload");
105        let json = serde_json::to_string(&d).unwrap();
106        let back: Digest = serde_json::from_str(&json).unwrap();
107        assert_eq!(d, back);
108    }
109}