Coverage Report

Created: 2026-07-21 15:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-util/src/digest_hasher.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use std::sync::OnceLock;
16
17
use blake3::Hasher as Blake3Hasher;
18
use bytes::BytesMut;
19
use futures::Future;
20
use nativelink_config::stores::ConfigDigestHashFunction;
21
use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err};
22
use nativelink_metric::{
23
    MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent,
24
};
25
use nativelink_proto::build::bazel::remote::execution::v2::digest_function::Value as ProtoDigestFunction;
26
use opentelemetry::context::Context;
27
use serde::{Deserialize, Serialize};
28
use sha2::{Digest, Sha256};
29
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt};
30
31
use crate::common::DigestInfo;
32
use crate::{fs, spawn_blocking};
33
34
static DEFAULT_DIGEST_HASHER_FUNC: OnceLock<DigestHasherFunc> = OnceLock::new();
35
36
/// Utility function to make a context with a specific hasher function set.
37
82
pub fn make_ctx_for_hash_func<H>(hasher: H) -> Result<Context, Error>
38
82
where
39
82
    H: TryInto<DigestHasherFunc>,
40
82
    H::Error: Into<Error>,
41
{
42
82
    let digest_hasher_func = hasher
43
82
        .try_into()
44
82
        .err_tip(|| "Could not convert into DigestHasherFunc")
?0
;
45
46
82
    let new_ctx = Context::current_with_value(digest_hasher_func);
47
48
82
    Ok(new_ctx)
49
82
}
50
51
/// Get the default hasher.
52
282
pub fn default_digest_hasher_func() -> DigestHasherFunc {
53
282
    *DEFAULT_DIGEST_HASHER_FUNC.get_or_init(|| DigestHasherFunc::Sha256)
54
282
}
55
56
/// Get the hasher requested by the client from the active context (set via
57
/// [`make_ctx_for_hash_func`]), falling back to the default hasher.
58
5
pub fn digest_hasher_func_from_context() -> DigestHasherFunc {
59
5
    Context::current()
60
5
        .get::<DigestHasherFunc>()
61
5
        .map_or_else(default_digest_hasher_func, |v| *v)
62
5
}
63
64
/// Sets the default hasher to use if no hasher was requested by the client.
65
0
pub fn set_default_digest_hasher_func(hasher: DigestHasherFunc) -> Result<(), Error> {
66
0
    DEFAULT_DIGEST_HASHER_FUNC
67
0
        .set(hasher)
68
0
        .map_err(|_| make_err!(Code::Internal, "default_digest_hasher_func already set"))
69
0
}
70
71
/// Supported digest hash functions.
72
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)]
73
pub enum DigestHasherFunc {
74
    Sha256,
75
    Blake3,
76
}
77
78
impl MetricsComponent for DigestHasherFunc {
79
0
    fn publish(
80
0
        &self,
81
0
        kind: MetricKind,
82
0
        field_metadata: MetricFieldData,
83
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
84
0
        format!("{self:?}").publish(kind, field_metadata)
85
0
    }
86
}
87
88
impl DigestHasherFunc {
89
5.24k
    pub fn hasher(&self) -> DigestHasherImpl {
90
5.24k
        self.into()
91
5.24k
    }
92
93
    #[must_use]
94
265
    pub const fn proto_digest_func(&self) -> ProtoDigestFunction {
95
265
        match self {
96
263
            Self::Sha256 => ProtoDigestFunction::Sha256,
97
2
            Self::Blake3 => ProtoDigestFunction::Blake3,
98
        }
99
265
    }
100
}
101
102
impl From<ConfigDigestHashFunction> for DigestHasherFunc {
103
0
    fn from(value: ConfigDigestHashFunction) -> Self {
104
0
        match value {
105
0
            ConfigDigestHashFunction::Sha256 => Self::Sha256,
106
0
            ConfigDigestHashFunction::Blake3 => Self::Blake3,
107
        }
108
0
    }
109
}
110
111
impl TryFrom<ProtoDigestFunction> for DigestHasherFunc {
112
    type Error = Error;
113
114
8
    fn try_from(value: ProtoDigestFunction) -> Result<Self, Self::Error> {
115
8
        match value {
116
6
            ProtoDigestFunction::Sha256 => Ok(Self::Sha256),
117
0
            ProtoDigestFunction::Blake3 => Ok(Self::Blake3),
118
2
            v => Err(make_input_err!(
119
2
                "Unknown or unsupported digest function for proto conversion {v:?}"
120
2
            )),
121
        }
122
8
    }
123
}
124
125
impl TryFrom<&str> for DigestHasherFunc {
126
    type Error = Error;
127
128
0
    fn try_from(value: &str) -> Result<Self, Self::Error> {
129
0
        match value.to_uppercase().as_str() {
130
0
            "SHA256" => Ok(Self::Sha256),
131
0
            "BLAKE3" => Ok(Self::Blake3),
132
0
            v => Err(make_input_err!(
133
0
                "Unknown or unsupported digest function for string conversion: {v:?}"
134
0
            )),
135
        }
136
0
    }
137
}
138
139
impl core::fmt::Display for DigestHasherFunc {
140
29
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141
29
        match self {
142
27
            Self::Sha256 => write!(f, "SHA256"),
143
2
            Self::Blake3 => write!(f, "BLAKE3"),
144
        }
145
29
    }
146
}
147
148
impl TryFrom<i32> for DigestHasherFunc {
149
    type Error = Error;
150
151
80
    fn try_from(value: i32) -> Result<Self, Self::Error> {
152
        // Zero means not-set.
153
80
        if value == 0 {
154
29
            return Ok(default_digest_hasher_func());
155
51
        }
156
51
        match ProtoDigestFunction::try_from(value) {
157
48
            Ok(ProtoDigestFunction::Sha256) => Ok(Self::Sha256),
158
3
            Ok(ProtoDigestFunction::Blake3) => Ok(Self::Blake3),
159
0
            value => Err(make_input_err!(
160
                "Unknown or unsupported digest function for int conversion: {:?}",
161
0
                value.map(|v| v.as_str_name())
162
            )),
163
        }
164
80
    }
165
}
166
167
impl From<&DigestHasherFunc> for DigestHasherImpl {
168
5.24k
    fn from(value: &DigestHasherFunc) -> Self {
169
5.24k
        let hash_func_impl = match value {
170
199
            DigestHasherFunc::Sha256 => DigestHasherFuncImpl::Sha256(Sha256::new()),
171
5.04k
            DigestHasherFunc::Blake3 => DigestHasherFuncImpl::Blake3(Box::default()),
172
        };
173
5.24k
        Self {
174
5.24k
            hashed_size: 0,
175
5.24k
            hash_func_impl,
176
5.24k
        }
177
5.24k
    }
178
}
179
180
/// Wrapper to compute a hash of arbitrary data.
181
pub trait DigestHasher {
182
    /// Update the hasher with some additional data.
183
    fn update(&mut self, input: &[u8]);
184
185
    /// Finalize the hash function and collect the results into a digest.
186
    fn finalize_digest(&mut self) -> DigestInfo;
187
188
    /// Specialized version of the hashing function that is optimized for
189
    /// handling files. These optimizations take into account things like,
190
    /// the file size and the hasher algorithm to decide how to best process
191
    /// the file and feed it into the hasher.
192
    fn digest_for_file(
193
        self,
194
        file_path: impl AsRef<std::path::Path>,
195
        file: fs::FileSlot,
196
        size_hint: Option<u64>,
197
    ) -> impl Future<Output = Result<(DigestInfo, fs::FileSlot), Error>>;
198
199
    /// Utility function to compute a hash from a generic reader.
200
13
    fn compute_from_reader<R: AsyncRead + Unpin + Send>(
201
13
        &mut self,
202
13
        mut reader: R,
203
13
    ) -> impl Future<Output = Result<DigestInfo, Error>> {
204
13
        async move {
205
13
            let mut chunk = BytesMut::with_capacity(fs::DEFAULT_READ_BUFF_SIZE);
206
            loop {
207
23
                reader
208
23
                    .read_buf(&mut chunk)
209
23
                    .await
210
23
                    .err_tip(|| "Could not read chunk during compute_from_reader")
?0
;
211
23
                if chunk.is_empty() {
212
13
                    break; // EOF.
213
10
                }
214
10
                DigestHasher::update(self, &chunk);
215
10
                chunk.clear();
216
            }
217
13
            Ok(DigestHasher::finalize_digest(self))
218
13
        }
219
13
    }
220
}
221
222
#[expect(
223
    variant_size_differences,
224
    reason = "some variants are already boxed; this is acceptable"
225
)]
226
#[derive(Debug)]
227
pub enum DigestHasherFuncImpl {
228
    Sha256(Sha256),
229
    Blake3(Box<Blake3Hasher>), // Box because Blake3Hasher is 1.3kb in size.
230
}
231
232
/// The individual implementation of the hash function.
233
#[derive(Debug)]
234
pub struct DigestHasherImpl {
235
    hashed_size: u64,
236
    hash_func_impl: DigestHasherFuncImpl,
237
}
238
239
impl DigestHasherImpl {
240
    #[inline]
241
0
    async fn hash_file(
242
0
        &mut self,
243
0
        mut file: fs::FileSlot,
244
13
    ) -> Result<(DigestInfo, fs::FileSlot), Error> {
245
13
        let digest = self
246
13
            .compute_from_reader(&mut file)
247
13
            .await
248
13
            .err_tip(|| "In digest_for_file")
?0
;
249
13
        Ok((digest, file))
250
13
    }
251
}
252
253
impl DigestHasher for DigestHasherImpl {
254
    #[inline]
255
5.25k
    fn update(&mut self, input: &[u8]) {
256
5.25k
        self.hashed_size += input.len() as u64;
257
5.25k
        match &mut self.hash_func_impl {
258
201
            DigestHasherFuncImpl::Sha256(h) => sha2::digest::Update::update(h, input),
259
5.04k
            DigestHasherFuncImpl::Blake3(h) => {
260
5.04k
                Blake3Hasher::update(h, input);
261
5.04k
            }
262
        }
263
5.25k
    }
264
265
    #[inline]
266
5.24k
    fn finalize_digest(&mut self) -> DigestInfo {
267
5.24k
        let hash = match &mut self.hash_func_impl {
268
198
            DigestHasherFuncImpl::Sha256(h) => h.finalize_reset().into(),
269
5.04k
            DigestHasherFuncImpl::Blake3(h) => h.finalize().into(),
270
        };
271
5.24k
        DigestInfo::new(hash, self.hashed_size)
272
5.24k
    }
273
274
13
    async fn digest_for_file(
275
13
        mut self,
276
13
        file_path: impl AsRef<std::path::Path>,
277
13
        mut file: fs::FileSlot,
278
13
        size_hint: Option<u64>,
279
13
    ) -> Result<(DigestInfo, fs::FileSlot), Error> {
280
13
        let file_position = file
281
13
            .stream_position()
282
13
            .await
283
13
            .err_tip(|| "Couldn't get stream position in digest_for_file")
?0
;
284
13
        if file_position != 0 {
285
0
            return self.hash_file(file).await;
286
13
        }
287
        // If we are a small file, it's faster to just do it the "slow" way.
288
        // Great read: https://github.com/david-slatinek/c-read-vs.-mmap
289
13
        if let Some(size_hint) = size_hint
290
13
            && size_hint <= fs::DEFAULT_READ_BUFF_SIZE as u64
291
        {
292
13
            return self.hash_file(file).await;
293
0
        }
294
0
        let file_path = file_path.as_ref().to_path_buf();
295
0
        match self.hash_func_impl {
296
0
            DigestHasherFuncImpl::Sha256(_) => self.hash_file(file).await,
297
0
            DigestHasherFuncImpl::Blake3(mut hasher) => {
298
0
                spawn_blocking!("digest_for_file", move || {
299
0
                    hasher.update_mmap(file_path).map_err(|e| {
300
0
                        Error::from_std_err(Code::Internal, &e)
301
0
                            .append("Error in blake3's update_mmap")
302
0
                    })?;
303
0
                    Result::<_, Error>::Ok((
304
0
                        DigestInfo::new(hasher.finalize().into(), hasher.count()),
305
0
                        file,
306
0
                    ))
307
0
                })
308
0
                .await
309
0
                .err_tip(|| "Could not spawn blocking task in digest_for_file")?
310
            }
311
        }
312
13
    }
313
}