Coverage Report

Created: 2025-11-06 19:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-util/src/common.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 core::cmp::{Eq, Ordering};
16
use core::hash::{BuildHasher, Hash};
17
use core::ops::{Deref, DerefMut};
18
use std::collections::HashMap;
19
use std::fmt;
20
use std::io::{Cursor, Write};
21
22
use bytes::{Buf, BufMut, Bytes, BytesMut};
23
use nativelink_error::{Error, ResultExt, make_input_err};
24
use nativelink_metric::{
25
    MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent,
26
};
27
use nativelink_proto::build::bazel::remote::execution::v2::Digest;
28
use prost::Message;
29
use serde::de::Visitor;
30
use serde::ser::Error as _;
31
use serde::{Deserialize, Deserializer, Serialize, Serializer};
32
use tracing::error;
33
34
pub use crate::fs;
35
36
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
37
#[repr(C)]
38
pub struct DigestInfo {
39
    /// Raw hash in packed form.
40
    packed_hash: PackedHash,
41
42
    /// Possibly the size of the digest in bytes.
43
    size_bytes: u64,
44
}
45
46
impl MetricsComponent for DigestInfo {
47
0
    fn publish(
48
0
        &self,
49
0
        _kind: MetricKind,
50
0
        field_metadata: MetricFieldData,
51
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
52
0
        format!("{self}").publish(MetricKind::String, field_metadata)
53
0
    }
54
}
55
56
impl DigestInfo {
57
5.42k
    pub const fn new(packed_hash: [u8; 32], size_bytes: u64) -> Self {
58
5.42k
        Self {
59
5.42k
            size_bytes,
60
5.42k
            packed_hash: PackedHash(packed_hash),
61
5.42k
        }
62
5.42k
    }
63
64
5.45k
    pub fn try_new<T>(hash: &str, size_bytes: T) -> Result<Self, Error>
65
5.45k
    where
66
5.45k
        T: TryInto<u64> + fmt::Display + Copy,
67
    {
68
5.44k
        let packed_hash =
69
5.45k
            PackedHash::from_hex(hash).err_tip(|| format!(
"Invalid sha256 hash: {hash}"9
))
?9
;
70
5.44k
        let size_bytes = size_bytes
71
5.44k
            .try_into()
72
5.44k
            .map_err(|_| make_input_err!("Could not convert {} into u64", size_bytes))
?0
;
73
        // The proto `Digest` takes an i64, so to keep compatibility
74
        // we only allow sizes that can fit into an i64.
75
5.44k
        if size_bytes > i64::MAX as u64 {
  Branch (75:12): [True: 0, False: 3]
  Branch (75:12): [True: 0, False: 166]
  Branch (75:12): [True: 0, False: 21]
  Branch (75:12): [True: 0, False: 4]
  Branch (75:12): [True: 0, False: 2]
  Branch (75:12): [True: 0, False: 4.58k]
  Branch (75:12): [Folded - Ignored]
  Branch (75:12): [True: 0, False: 1]
  Branch (75:12): [True: 0, False: 6]
  Branch (75:12): [True: 0, False: 8]
  Branch (75:12): [True: 0, False: 1]
  Branch (75:12): [True: 0, False: 6]
  Branch (75:12): [True: 2, False: 5]
  Branch (75:12): [True: 0, False: 6]
  Branch (75:12): [True: 0, False: 9]
  Branch (75:12): [True: 0, False: 58]
  Branch (75:12): [True: 0, False: 5]
  Branch (75:12): [True: 0, False: 1]
  Branch (75:12): [True: 0, False: 11]
  Branch (75:12): [True: 0, False: 18]
  Branch (75:12): [True: 0, False: 10]
  Branch (75:12): [True: 0, False: 2]
  Branch (75:12): [True: 0, False: 405]
  Branch (75:12): [True: 0, False: 1]
  Branch (75:12): [True: 0, False: 2]
  Branch (75:12): [True: 0, False: 0]
  Branch (75:12): [True: 0, False: 0]
  Branch (75:12): [True: 0, False: 2]
  Branch (75:12): [Folded - Ignored]
  Branch (75:12): [True: 0, False: 1]
  Branch (75:12): [True: 0, False: 1]
  Branch (75:12): [True: 0, False: 7]
  Branch (75:12): [True: 0, False: 0]
  Branch (75:12): [True: 0, False: 3]
  Branch (75:12): [True: 0, False: 2]
  Branch (75:12): [True: 0, False: 3]
  Branch (75:12): [True: 0, False: 1]
  Branch (75:12): [True: 0, False: 6]
  Branch (75:12): [True: 0, False: 6]
  Branch (75:12): [True: 0, False: 27]
  Branch (75:12): [True: 0, False: 3]
  Branch (75:12): [True: 0, False: 3]
  Branch (75:12): [True: 0, False: 4]
  Branch (75:12): [True: 0, False: 3]
  Branch (75:12): [True: 0, False: 13]
  Branch (75:12): [True: 0, False: 12]
  Branch (75:12): [True: 0, False: 10]
76
2
            return Err(make_input_err!(
77
2
                "Size bytes is too large: {} - max: {}",
78
2
                size_bytes,
79
2
                i64::MAX
80
2
            ));
81
5.44k
        }
82
5.44k
        Ok(Self {
83
5.44k
            packed_hash,
84
5.44k
            size_bytes,
85
5.44k
        })
86
5.45k
    }
87
88
29
    pub const fn zero_digest() -> Self {
89
29
        Self {
90
29
            size_bytes: 0,
91
29
            packed_hash: PackedHash::new(),
92
29
        }
93
29
    }
94
95
40.3k
    pub const fn packed_hash(&self) -> &PackedHash {
96
40.3k
        &self.packed_hash
97
40.3k
    }
98
99
143
    pub const fn set_packed_hash(&mut self, packed_hash: [u8; 32]) {
100
143
        self.packed_hash = PackedHash(packed_hash);
101
143
    }
102
103
16.2k
    pub const fn size_bytes(&self) -> u64 {
104
16.2k
        self.size_bytes
105
16.2k
    }
106
107
    /// Returns a struct that can turn the `DigestInfo` into a string.
108
909
    const fn stringifier(&self) -> DigestStackStringifier<'_> {
109
909
        DigestStackStringifier::new(self)
110
909
    }
111
}
112
113
/// Counts the number of digits a number needs if it were to be
114
/// converted to a string.
115
0
const fn count_digits(mut num: u64) -> usize {
116
0
    let mut count = 0;
117
0
    while num != 0 {
  Branch (117:11): [Folded - Ignored]
  Branch (117:11): [Folded - Ignored]
118
0
        count += 1;
119
0
        num /= 10;
120
0
    }
121
0
    count
122
0
}
123
124
/// An optimized version of a function that can convert a `DigestInfo`
125
/// into a str on the stack.
126
struct DigestStackStringifier<'a> {
127
    digest: &'a DigestInfo,
128
    /// Buffer that can hold the string representation of the `DigestInfo`.
129
    /// - Hex is '2 * sizeof(PackedHash)'.
130
    /// - Digits can be at most `count_digits(u64::MAX)`.
131
    /// - We also have a hyphen separator.
132
    buf: [u8; size_of::<PackedHash>() * 2 + count_digits(u64::MAX) + 1],
133
}
134
135
impl<'a> DigestStackStringifier<'a> {
136
909
    const fn new(digest: &'a DigestInfo) -> Self {
137
909
        DigestStackStringifier {
138
909
            digest,
139
909
            buf: [b'-'; size_of::<PackedHash>() * 2 + count_digits(u64::MAX) + 1],
140
909
        }
141
909
    }
142
143
909
    fn as_str(&mut self) -> Result<&str, Error> {
144
        // Populate the buffer and return the amount of bytes written
145
        // to the buffer.
146
909
        let len = {
147
909
            let mut cursor = Cursor::new(&mut self.buf[..]);
148
909
            let hex = self.digest.packed_hash.to_hex().map_err(|e| 
{0
149
0
                make_input_err!(
150
                    "Could not convert PackedHash to hex - {e:?} - {:?}",
151
                    self.digest
152
                )
153
0
            })?;
154
909
            cursor
155
909
                .write_all(&hex)
156
909
                .err_tip(|| format!(
"Could not write hex to buffer - {hex:?} - {hex:?}"0
,))
?0
;
157
            // Note: We already have a hyphen at this point because we
158
            // initialized the buffer with hyphens.
159
909
            cursor.advance(1);
160
909
            cursor
161
909
                .write_fmt(format_args!("{}", self.digest.size_bytes()))
162
909
                .err_tip(|| format!(
"Could not write size_bytes to buffer - {hex:?}"0
,))
?0
;
163
909
            cursor
164
909
                .position()
165
909
                .try_into()
166
909
                .map_err(|e| make_input_err!("Cursor position exceeds usize bounds: {e}"))
?0
167
        };
168
        // Convert the buffer into utf8 string.
169
909
        core::str::from_utf8(&self.buf[..len]).map_err(|e| 
{0
170
0
            make_input_err!(
171
                "Could not convert [u8] to string - {} - {:?} - {:?}",
172
                self.digest,
173
                self.buf,
174
                e,
175
            )
176
0
        })
177
909
    }
178
}
179
180
/// Custom serializer for `DigestInfo` because the default Serializer
181
/// would try to encode the data as a byte array, but we use {hex}-{size}.
182
impl Serialize for DigestInfo {
183
181
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
184
181
    where
185
181
        S: Serializer,
186
    {
187
181
        let mut stringifier = self.stringifier();
188
181
        serializer.serialize_str(
189
181
            stringifier
190
181
                .as_str()
191
181
                .err_tip(|| "During serialization of DigestInfo")
192
181
                .map_err(S::Error::custom)
?0
,
193
        )
194
181
    }
195
}
196
197
/// Custom deserializer for `DigestInfo` because the default Deserializer
198
/// would try to decode the data as a byte array, but we use {hex}-{size}.
199
impl<'de> Deserialize<'de> for DigestInfo {
200
4.76k
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
201
4.76k
    where
202
4.76k
        D: Deserializer<'de>,
203
    {
204
        struct DigestInfoVisitor;
205
        impl Visitor<'_> for DigestInfoVisitor {
206
            type Value = DigestInfo;
207
208
0
            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
209
0
                formatter.write_str("a string representing a DigestInfo")
210
0
            }
211
212
4.76k
            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
213
4.76k
            where
214
4.76k
                E: serde::de::Error,
215
            {
216
4.76k
                let Some((hash, size)) = s.split_once('-') else {
  Branch (216:21): [True: 166, False: 0]
  Branch (216:21): [True: 0, False: 0]
  Branch (216:21): [True: 4.58k, False: 0]
  Branch (216:21): [Folded - Ignored]
  Branch (216:21): [True: 6, False: 0]
  Branch (216:21): [True: 3, False: 0]
  Branch (216:21): [Folded - Ignored]
  Branch (216:21): [True: 0, False: 0]
217
0
                    return Err(E::custom(
218
0
                        "Invalid DigestInfo format, expected '-' separator",
219
0
                    ));
220
                };
221
4.76k
                let size_bytes = size
222
4.76k
                    .parse::<u64>()
223
4.76k
                    .map_err(|e| 
E::custom0
(
format!0
(
"Could not parse size_bytes: {e:?}"0
)))
?0
;
224
4.76k
                DigestInfo::try_new(hash, size_bytes)
225
4.76k
                    .map_err(|e| 
E::custom1
(
format!1
(
"Could not create DigestInfo: {e:?}"1
)))
226
4.76k
            }
227
        }
228
4.76k
        deserializer.deserialize_str(DigestInfoVisitor)
229
4.76k
    }
230
}
231
232
impl fmt::Display for DigestInfo {
233
586
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234
586
        let mut stringifier = self.stringifier();
235
586
        f.write_str(
236
586
            stringifier
237
586
                .as_str()
238
586
                .err_tip(|| "During serialization of DigestInfo")
239
586
                .map_err(|e| 
{0
240
0
                    error!("Could not convert DigestInfo to string - {e:?}");
241
0
                    fmt::Error
242
0
                })?,
243
        )
244
586
    }
245
}
246
247
impl fmt::Debug for DigestInfo {
248
142
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249
142
        let mut stringifier = self.stringifier();
250
142
        match stringifier.as_str() {
251
142
            Ok(s) => f.debug_tuple("DigestInfo").field(&s).finish(),
252
0
            Err(e) => {
253
0
                error!("Could not convert DigestInfo to string - {e:?}");
254
0
                Err(fmt::Error)
255
            }
256
        }
257
142
    }
258
}
259
260
impl Ord for DigestInfo {
261
0
    fn cmp(&self, other: &Self) -> Ordering {
262
0
        self.packed_hash
263
0
            .cmp(&other.packed_hash)
264
0
            .then_with(|| self.size_bytes.cmp(&other.size_bytes))
265
0
    }
266
}
267
268
impl PartialOrd for DigestInfo {
269
0
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
270
0
        Some(self.cmp(other))
271
0
    }
272
}
273
274
impl TryFrom<Digest> for DigestInfo {
275
    type Error = Error;
276
277
205
    fn try_from(digest: Digest) -> Result<Self, Self::Error> {
278
205
        let 
packed_hash204
= PackedHash::from_hex(&digest.hash)
279
205
            .err_tip(|| format!(
"Invalid sha256 hash: {}"1
, digest.hash))
?1
;
280
204
        let size_bytes = digest
281
204
            .size_bytes
282
204
            .try_into()
283
204
            .map_err(|_| make_input_err!("Could not convert {} into u64", digest.size_bytes))
?0
;
284
204
        Ok(Self {
285
204
            packed_hash,
286
204
            size_bytes,
287
204
        })
288
205
    }
289
}
290
291
impl TryFrom<&Digest> for DigestInfo {
292
    type Error = Error;
293
294
0
    fn try_from(digest: &Digest) -> Result<Self, Self::Error> {
295
0
        let packed_hash = PackedHash::from_hex(&digest.hash)
296
0
            .err_tip(|| format!("Invalid sha256 hash: {}", digest.hash))?;
297
0
        let size_bytes = digest
298
0
            .size_bytes
299
0
            .try_into()
300
0
            .map_err(|_| make_input_err!("Could not convert {} into u64", digest.size_bytes))?;
301
0
        Ok(Self {
302
0
            packed_hash,
303
0
            size_bytes,
304
0
        })
305
0
    }
306
}
307
308
impl From<DigestInfo> for Digest {
309
234
    fn from(val: DigestInfo) -> Self {
310
        Self {
311
234
            hash: val.packed_hash.to_string(),
312
234
            size_bytes: val.size_bytes.try_into().unwrap_or_else(|e| 
{0
313
0
                error!("Could not convert {} into u64 - {e:?}", val.size_bytes);
314
                // This is a placeholder value that can help a user identify
315
                // that the conversion failed.
316
0
                -255
317
0
            }),
318
        }
319
234
    }
320
}
321
322
impl From<&DigestInfo> for Digest {
323
10
    fn from(val: &DigestInfo) -> Self {
324
        Self {
325
10
            hash: val.packed_hash.to_string(),
326
10
            size_bytes: val.size_bytes.try_into().unwrap_or_else(|e| 
{0
327
0
                error!("Could not convert {} into u64 - {e:?}", val.size_bytes);
328
                // This is a placeholder value that can help a user identify
329
                // that the conversion failed.
330
0
                -255
331
0
            }),
332
        }
333
10
    }
334
}
335
336
#[derive(
337
    Debug, Serialize, Deserialize, Default, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord,
338
)]
339
pub struct PackedHash([u8; 32]);
340
341
const SIZE_OF_PACKED_HASH: usize = 32;
342
impl PackedHash {
343
0
    const fn new() -> Self {
344
0
        Self([0; SIZE_OF_PACKED_HASH])
345
0
    }
346
347
5.66k
    fn from_hex(hash: &str) -> Result<Self, Error> {
348
5.66k
        let mut packed_hash = [0u8; 32];
349
5.66k
        hex::decode_to_slice(hash, &mut packed_hash)
350
5.66k
            .map_err(|e| make_input_err!("Invalid sha256 hash: {hash} - {e:?}"))
?10
;
351
5.65k
        Ok(Self(packed_hash))
352
5.66k
    }
353
354
    /// Converts the packed hash into a hex string.
355
    #[inline]
356
1.18k
    fn to_hex(self) -> Result<[u8; SIZE_OF_PACKED_HASH * 2], fmt::Error> {
357
1.18k
        let mut hash = [0u8; SIZE_OF_PACKED_HASH * 2];
358
1.18k
        hex::encode_to_slice(self.0, &mut hash).map_err(|e| 
{0
359
0
            error!("Could not convert PackedHash to hex - {e:?} - {:?}", self.0);
360
0
            fmt::Error
361
0
        })?;
362
1.18k
        Ok(hash)
363
1.18k
    }
364
}
365
366
impl fmt::Display for PackedHash {
367
276
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368
276
        let hash = self.to_hex()
?0
;
369
276
        match core::str::from_utf8(&hash) {
370
276
            Ok(hash) => f.write_str(hash)
?0
,
371
0
            Err(_) => f.write_str(&format!("Could not convert hash to utf8 {:?}", self.0))?,
372
        }
373
276
        Ok(())
374
276
    }
375
}
376
377
impl Deref for PackedHash {
378
    type Target = [u8; 32];
379
380
40.2k
    fn deref(&self) -> &Self::Target {
381
40.2k
        &self.0
382
40.2k
    }
383
}
384
385
impl DerefMut for PackedHash {
386
143
    fn deref_mut(&mut self) -> &mut Self::Target {
387
143
        &mut self.0
388
143
    }
389
}
390
391
// Simple utility trait that makes it easier to apply `.try_map` to Vec.
392
// This will convert one vector into another vector with a different type.
393
pub trait VecExt<T> {
394
    fn try_map<F, U>(self, f: F) -> Result<Vec<U>, Error>
395
    where
396
        Self: Sized,
397
        F: (Fn(T) -> Result<U, Error>) + Sized;
398
}
399
400
impl<T> VecExt<T> for Vec<T> {
401
12
    fn try_map<F, U>(self, f: F) -> Result<Vec<U>, Error>
402
12
    where
403
12
        Self: Sized,
404
12
        F: (Fn(T) -> Result<U, Error>) + Sized,
405
    {
406
12
        let mut output = Vec::with_capacity(self.len());
407
18
        for 
item6
in self {
408
6
            output.push((f)(item)
?0
);
409
        }
410
12
        Ok(output)
411
12
    }
412
}
413
414
// Simple utility trait that makes it easier to apply `.try_map` to HashMap.
415
// This will convert one HashMap into another keeping the key the same, but
416
// different value type.
417
pub trait HashMapExt<K: Eq + Hash, T, S: BuildHasher> {
418
    fn try_map<F, U>(self, f: F) -> Result<HashMap<K, U, S>, Error>
419
    where
420
        Self: Sized,
421
        F: (Fn(T) -> Result<U, Error>) + Sized;
422
}
423
424
impl<K: Eq + Hash, T, S: BuildHasher + Clone> HashMapExt<K, T, S> for HashMap<K, T, S> {
425
3
    fn try_map<F, U>(self, f: F) -> Result<HashMap<K, U, S>, Error>
426
3
    where
427
3
        Self: Sized,
428
3
        F: (Fn(T) -> Result<U, Error>) + Sized,
429
    {
430
3
        let mut output = HashMap::with_capacity_and_hasher(self.len(), (*self.hasher()).clone());
431
5
        for (
k2
,
v2
) in self {
432
2
            output.insert(k, (f)(v)
?0
);
433
        }
434
3
        Ok(output)
435
3
    }
436
}
437
438
// Utility to encode our proto into GRPC stream format.
439
38
pub fn encode_stream_proto<T: Message>(proto: &T) -> Result<Bytes, Box<dyn core::error::Error>> {
440
    // See below comment on spec.
441
    use core::mem::size_of;
442
    const PREFIX_BYTES: usize = size_of::<u8>() + size_of::<u32>();
443
444
38
    let mut buf = BytesMut::new();
445
446
228
    for _ in 0..PREFIX_BYTES {
447
190
        // Advance our buffer first.
448
190
        // We will backfill it once we know the size of the message.
449
190
        buf.put_u8(0);
450
190
    }
451
38
    proto.encode(&mut buf)
?0
;
452
38
    let len = buf.len() - PREFIX_BYTES;
453
    {
454
38
        let mut buf = &mut buf[0..PREFIX_BYTES];
455
        // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#:~:text=Compressed-Flag
456
        // for more details on spec.
457
        // Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer.
458
38
        buf.put_u8(0);
459
        // Message-Length -> {length of Message} # encoded as 4 byte unsigned integer (big endian).
460
38
        buf.put_u32(u32::try_from(len)
?0
);
461
        // Message -> *{binary octet}.
462
    }
463
464
38
    Ok(buf.freeze())
465
38
}
466
467
/// Small utility to reseed the global RNG.
468
/// Done this way because we use it in a macro
469
/// and macro's can't load external crates.
470
#[inline]
471
403
pub fn reseed_rng_for_test() -> Result<(), Error> {
472
403
    rand::rng()
473
403
        .reseed()
474
403
        .map_err(|e| make_input_err!("Could not reseed RNG - {e:?}"))
475
403
}