cargo/core/compiler/fingerprint/
dep_info.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
//! Types and functions managing dep-info files.
//! For more, see [the documentation] in the `fingerprint` module.
//!
//! [the documentation]: crate::core::compiler::fingerprint#dep-info-files

use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt;
use std::io;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use std::str::FromStr;
use std::sync::Arc;

use anyhow::bail;
use cargo_util::paths;
use cargo_util::ProcessBuilder;
use cargo_util::Sha256;

use crate::CargoResult;
use crate::CARGO_ENV;

/// The current format version of [`EncodedDepInfo`].
const CURRENT_ENCODED_DEP_INFO_VERSION: u8 = 1;

/// The representation of the `.d` dep-info file generated by rustc
#[derive(Default)]
pub struct RustcDepInfo {
    /// The list of files that the main target in the dep-info file depends on.
    ///
    /// The optional checksums are parsed from the special `# checksum:...` comments.
    pub files: HashMap<PathBuf, Option<(u64, Checksum)>>,
    /// The list of environment variables we found that the rustc compilation
    /// depends on.
    ///
    /// The first element of the pair is the name of the env var and the second
    /// item is the value. `Some` means that the env var was set, and `None`
    /// means that the env var wasn't actually set and the compilation depends
    /// on it not being set.
    ///
    /// These are from the special `# env-var:...` comments.
    pub env: Vec<(String, Option<String>)>,
}

/// Tells the associated path in [`EncodedDepInfo::files`] is relative to package root,
/// target root, or absolute.
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
pub enum DepInfoPathType {
    /// src/, e.g. src/lib.rs
    PackageRootRelative,
    /// target/debug/deps/lib...
    /// or an absolute path /.../sysroot/...
    TargetRootRelative,
}

/// Same as [`RustcDepInfo`] except avoids absolute paths as much as possible to
/// allow moving around the target directory.
///
/// This is also stored in an optimized format to make parsing it fast because
/// Cargo will read it for crates on all future compilations.
///
/// Currently the format looks like:
///
/// ```text
/// +--------+---------+------------+------------+---------------+---------------+
/// | marker | version | # of files | file paths | # of env vars | env var pairs |
/// +--------+---------+------------+------------+---------------+---------------+
/// ```
///
/// Each field represents
///
/// * _Marker_ --- A magic marker to ensure that older Cargoes, which only
///   recognize format v0 (prior to checksum support in [`f4ca7390`]), do not
///   proceed with parsing newer formats. Since [`EncodedDepInfo`] is merely
///   an optimization, and to avoid adding complexity, Cargo recognizes only
///   one version of [`CURRENT_ENCODED_DEP_INFO_VERSION`].
///   The current layout looks like this
///   ```text
///   +----------------------------+
///   | [0x01 0x00 0x00 0x00 0xff] |
///   +----------------------------+
///   ```
///   These bytes will be interpreted as "one file tracked and an invalid
///   [`DepInfoPathType`] variant with 255" by older Cargoes, causing them to
///   stop parsing. This could prevent problematic parsing as noted in
///   rust-lang/cargo#14712.
/// * _Version_ --- The current format version.
/// * _Number of files/envs_ --- A `u32` representing the number of things.
/// * _File paths_ --- Zero or more paths of files the dep-info file depends on.
///   Each path is encoded as the following:
///
///   ```text
///   +-----------+-------------+------------+---------------+-----------+-------+
///   | path type | len of path | path bytes | cksum exists? | file size | cksum |
///   +-----------+-------------+------------+---------------+-----------+-------+
///   ```
/// * _Env var pairs_ --- Zero or more env vars the dep-info file depends on.
///   Each env key-value pair is encoded as the following:
///   ```text
///   +------------+-----------+---------------+--------------+-------------+
///   | len of key | key bytes | value exists? | len of value | value bytes |
///   +------------+-----------+---------------+--------------+-------------+
///   ```
///
/// [`f4ca7390`]: https://github.com/rust-lang/cargo/commit/f4ca739073185ea5e1148ff100bb4a06d3bf721d
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EncodedDepInfo {
    pub files: Vec<(DepInfoPathType, PathBuf, Option<(u64, String)>)>,
    pub env: Vec<(String, Option<String>)>,
}

impl EncodedDepInfo {
    pub fn parse(mut bytes: &[u8]) -> Option<EncodedDepInfo> {
        let bytes = &mut bytes;
        read_magic_marker(bytes)?;
        let version = read_u8(bytes)?;
        if version != CURRENT_ENCODED_DEP_INFO_VERSION {
            return None;
        }

        let nfiles = read_usize(bytes)?;
        let mut files = Vec::with_capacity(nfiles);
        for _ in 0..nfiles {
            let ty = match read_u8(bytes)? {
                0 => DepInfoPathType::PackageRootRelative,
                1 => DepInfoPathType::TargetRootRelative,
                _ => return None,
            };
            let path_bytes = read_bytes(bytes)?;
            let path = paths::bytes2path(path_bytes).ok()?;
            let has_checksum = read_bool(bytes)?;
            let checksum_info = has_checksum
                .then(|| {
                    let file_len = read_u64(bytes);
                    let checksum_string = read_bytes(bytes)
                        .map(Vec::from)
                        .and_then(|v| String::from_utf8(v).ok());
                    file_len.zip(checksum_string)
                })
                .flatten();
            files.push((ty, path, checksum_info));
        }

        let nenv = read_usize(bytes)?;
        let mut env = Vec::with_capacity(nenv);
        for _ in 0..nenv {
            let key = str::from_utf8(read_bytes(bytes)?).ok()?.to_string();
            let val = match read_u8(bytes)? {
                0 => None,
                1 => Some(str::from_utf8(read_bytes(bytes)?).ok()?.to_string()),
                _ => return None,
            };
            env.push((key, val));
        }
        return Some(EncodedDepInfo { files, env });

        /// See [`EncodedDepInfo`] for why a magic marker exists.
        fn read_magic_marker(bytes: &mut &[u8]) -> Option<()> {
            let _size = read_usize(bytes)?;
            let path_type = read_u8(bytes)?;
            if path_type != u8::MAX {
                // Old depinfo. Give up parsing it.
                None
            } else {
                Some(())
            }
        }

        fn read_usize(bytes: &mut &[u8]) -> Option<usize> {
            let ret = bytes.get(..4)?;
            *bytes = &bytes[4..];
            Some(u32::from_le_bytes(ret.try_into().unwrap()) as usize)
        }

        fn read_u64(bytes: &mut &[u8]) -> Option<u64> {
            let ret = bytes.get(..8)?;
            *bytes = &bytes[8..];
            Some(u64::from_le_bytes(ret.try_into().unwrap()))
        }

        fn read_bool(bytes: &mut &[u8]) -> Option<bool> {
            read_u8(bytes).map(|b| b != 0)
        }

        fn read_u8(bytes: &mut &[u8]) -> Option<u8> {
            let ret = *bytes.get(0)?;
            *bytes = &bytes[1..];
            Some(ret)
        }

        fn read_bytes<'a>(bytes: &mut &'a [u8]) -> Option<&'a [u8]> {
            let n = read_usize(bytes)? as usize;
            let ret = bytes.get(..n)?;
            *bytes = &bytes[n..];
            Some(ret)
        }
    }

    pub fn serialize(&self) -> CargoResult<Vec<u8>> {
        let mut ret = Vec::new();
        let dst = &mut ret;

        write_magic_marker(dst);
        dst.push(CURRENT_ENCODED_DEP_INFO_VERSION);

        write_usize(dst, self.files.len());
        for (ty, file, checksum_info) in self.files.iter() {
            match ty {
                DepInfoPathType::PackageRootRelative => dst.push(0),
                DepInfoPathType::TargetRootRelative => dst.push(1),
            }
            write_bytes(dst, paths::path2bytes(file)?);
            write_bool(dst, checksum_info.is_some());
            if let Some((len, checksum)) = checksum_info {
                write_u64(dst, *len);
                write_bytes(dst, checksum);
            }
        }

        write_usize(dst, self.env.len());
        for (key, val) in self.env.iter() {
            write_bytes(dst, key);
            match val {
                None => dst.push(0),
                Some(val) => {
                    dst.push(1);
                    write_bytes(dst, val);
                }
            }
        }
        return Ok(ret);

        /// See [`EncodedDepInfo`] for why a magic marker exists.
        ///
        /// There is an assumption that there is always at least a file.
        fn write_magic_marker(dst: &mut Vec<u8>) {
            write_usize(dst, 1);
            dst.push(u8::MAX);
        }

        fn write_bytes(dst: &mut Vec<u8>, val: impl AsRef<[u8]>) {
            let val = val.as_ref();
            write_usize(dst, val.len());
            dst.extend_from_slice(val);
        }

        fn write_usize(dst: &mut Vec<u8>, val: usize) {
            dst.extend(&u32::to_le_bytes(val as u32));
        }

        fn write_u64(dst: &mut Vec<u8>, val: u64) {
            dst.extend(&u64::to_le_bytes(val));
        }

        fn write_bool(dst: &mut Vec<u8>, val: bool) {
            dst.push(u8::from(val));
        }
    }
}

/// Parses the dep-info file coming out of rustc into a Cargo-specific format.
///
/// This function will parse `rustc_dep_info` as a makefile-style dep info to
/// learn about the all files which a crate depends on. This is then
/// re-serialized into the `cargo_dep_info` path in a Cargo-specific format.
///
/// The `pkg_root` argument here is the absolute path to the directory
/// containing `Cargo.toml` for this crate that was compiled. The paths listed
/// in the rustc dep-info file may or may not be absolute but we'll want to
/// consider all of them relative to the `root` specified.
///
/// The `rustc_cwd` argument is the absolute path to the cwd of the compiler
/// when it was invoked.
///
/// If the `allow_package` argument is true, then package-relative paths are
/// included. If it is false, then package-relative paths are skipped and
/// ignored (typically used for registry or git dependencies where we assume
/// the source never changes, and we don't want the cost of running `stat` on
/// all those files). See the module-level docs for the note about
/// `-Zbinary-dep-depinfo` for more details on why this is done.
///
/// The serialized Cargo format will contain a list of files, all of which are
/// relative if they're under `root`. or absolute if they're elsewhere.
///
/// The `env_config` argument is a set of environment variables that are
/// defined in `[env]` table of the `config.toml`.
pub fn translate_dep_info(
    rustc_dep_info: &Path,
    cargo_dep_info: &Path,
    rustc_cwd: &Path,
    pkg_root: &Path,
    target_root: &Path,
    rustc_cmd: &ProcessBuilder,
    allow_package: bool,
    env_config: &Arc<HashMap<String, OsString>>,
) -> CargoResult<()> {
    let depinfo = parse_rustc_dep_info(rustc_dep_info)?;

    let target_root = crate::util::try_canonicalize(target_root)?;
    let pkg_root = crate::util::try_canonicalize(pkg_root)?;
    let mut on_disk_info = EncodedDepInfo::default();
    on_disk_info.env = depinfo.env;

    // This is a bit of a tricky statement, but here we're *removing* the
    // dependency on environment variables that were defined specifically for
    // the command itself. Environment variables returned by `get_envs` includes
    // environment variables like:
    //
    // * `OUT_DIR` if applicable
    // * env vars added by a build script, if any
    //
    // The general idea here is that the dep info file tells us what, when
    // changed, should cause us to rebuild the crate. These environment
    // variables are synthesized by Cargo and/or the build script, and the
    // intention is that their values are tracked elsewhere for whether the
    // crate needs to be rebuilt.
    //
    // For example a build script says when it needs to be rerun and otherwise
    // it's assumed to produce the same output, so we're guaranteed that env
    // vars defined by the build script will always be the same unless the build
    // script itself reruns, in which case the crate will rerun anyway.
    //
    // For things like `OUT_DIR` it's a bit sketchy for now. Most of the time
    // that's used for code generation but this is technically buggy where if
    // you write a binary that does `println!("{}", env!("OUT_DIR"))` we won't
    // recompile that if you move the target directory. Hopefully that's not too
    // bad of an issue for now...
    //
    // This also includes `CARGO` since if the code is explicitly wanting to
    // know that path, it should be rebuilt if it changes. The CARGO path is
    // not tracked elsewhere in the fingerprint.
    //
    // For cargo#13280, We trace env vars that are defined in the `[env]` config table.
    on_disk_info.env.retain(|(key, _)| {
        env_config.contains_key(key) || !rustc_cmd.get_envs().contains_key(key) || key == CARGO_ENV
    });

    let serialize_path = |file| {
        // The path may be absolute or relative, canonical or not. Make sure
        // it is canonicalized so we are comparing the same kinds of paths.
        let abs_file = rustc_cwd.join(file);
        // If canonicalization fails, just use the abs path. There is currently
        // a bug where --remap-path-prefix is affecting .d files, causing them
        // to point to non-existent paths.
        let canon_file =
            crate::util::try_canonicalize(&abs_file).unwrap_or_else(|_| abs_file.clone());

        let (ty, path) = if let Ok(stripped) = canon_file.strip_prefix(&target_root) {
            (DepInfoPathType::TargetRootRelative, stripped)
        } else if let Ok(stripped) = canon_file.strip_prefix(&pkg_root) {
            if !allow_package {
                return None;
            }
            (DepInfoPathType::PackageRootRelative, stripped)
        } else {
            // It's definitely not target root relative, but this is an absolute path (since it was
            // joined to rustc_cwd) and as such re-joining it later to the target root will have no
            // effect.
            (DepInfoPathType::TargetRootRelative, &*abs_file)
        };
        Some((ty, path.to_owned()))
    };

    for (file, checksum_info) in depinfo.files {
        let Some((path_type, path)) = serialize_path(file) else {
            continue;
        };
        on_disk_info.files.push((
            path_type,
            path,
            checksum_info.map(|(len, checksum)| (len, checksum.to_string())),
        ));
    }
    paths::write(cargo_dep_info, on_disk_info.serialize()?)?;
    Ok(())
}

/// Parse the `.d` dep-info file generated by rustc.
pub fn parse_rustc_dep_info(rustc_dep_info: &Path) -> CargoResult<RustcDepInfo> {
    let contents = paths::read(rustc_dep_info)?;
    let mut ret = RustcDepInfo::default();
    let mut found_deps = false;

    for line in contents.lines() {
        if let Some(rest) = line.strip_prefix("# env-dep:") {
            let mut parts = rest.splitn(2, '=');
            let Some(env_var) = parts.next() else {
                continue;
            };
            let env_val = match parts.next() {
                Some(s) => Some(unescape_env(s)?),
                None => None,
            };
            ret.env.push((unescape_env(env_var)?, env_val));
        } else if let Some(pos) = line.find(": ") {
            if found_deps {
                continue;
            }
            found_deps = true;
            let mut deps = line[pos + 2..].split_whitespace();

            while let Some(s) = deps.next() {
                let mut file = s.to_string();
                while file.ends_with('\\') {
                    file.pop();
                    file.push(' ');
                    file.push_str(deps.next().ok_or_else(|| {
                        crate::util::internal("malformed dep-info format, trailing \\")
                    })?);
                }
                ret.files.entry(file.into()).or_default();
            }
        } else if let Some(rest) = line.strip_prefix("# checksum:") {
            let mut parts = rest.splitn(3, ' ');
            let Some(checksum) = parts.next().map(Checksum::from_str).transpose()? else {
                continue;
            };
            let Some(Ok(file_len)) = parts
                .next()
                .and_then(|s| s.strip_prefix("file_len:").map(|s| s.parse::<u64>()))
            else {
                continue;
            };
            let Some(path) = parts.next().map(PathBuf::from) else {
                continue;
            };

            ret.files.insert(path, Some((file_len, checksum)));
        }
    }
    return Ok(ret);

    // rustc tries to fit env var names and values all on a single line, which
    // means it needs to escape `\r` and `\n`. The escape syntax used is "\n"
    // which means that `\` also needs to be escaped.
    fn unescape_env(s: &str) -> CargoResult<String> {
        let mut ret = String::with_capacity(s.len());
        let mut chars = s.chars();
        while let Some(c) = chars.next() {
            if c != '\\' {
                ret.push(c);
                continue;
            }
            match chars.next() {
                Some('\\') => ret.push('\\'),
                Some('n') => ret.push('\n'),
                Some('r') => ret.push('\r'),
                Some(c) => bail!("unknown escape character `{}`", c),
                None => bail!("unterminated escape character"),
            }
        }
        Ok(ret)
    }
}

/// Parses Cargo's internal [`EncodedDepInfo`] structure that was previously
/// serialized to disk.
///
/// Note that this is not rustc's `*.d` files.
///
/// Also note that rustc's `*.d` files are translated to Cargo-specific
/// `EncodedDepInfo` files after compilations have finished in
/// [`translate_dep_info`].
///
/// Returns `None` if the file is corrupt or couldn't be read from disk. This
/// indicates that the crate should likely be rebuilt.
pub fn parse_dep_info(
    pkg_root: &Path,
    target_root: &Path,
    dep_info: &Path,
) -> CargoResult<Option<RustcDepInfo>> {
    let Ok(data) = paths::read_bytes(dep_info) else {
        return Ok(None);
    };
    let Some(info) = EncodedDepInfo::parse(&data) else {
        tracing::warn!("failed to parse cargo's dep-info at {:?}", dep_info);
        return Ok(None);
    };
    let mut ret = RustcDepInfo::default();
    ret.env = info.env;
    ret.files
        .extend(info.files.into_iter().map(|(ty, path, checksum_info)| {
            (
                make_absolute_path(ty, pkg_root, target_root, path),
                checksum_info.and_then(|(file_len, checksum)| {
                    Checksum::from_str(&checksum).ok().map(|c| (file_len, c))
                }),
            )
        }));
    Ok(Some(ret))
}

fn make_absolute_path(
    ty: DepInfoPathType,
    pkg_root: &Path,
    target_root: &Path,
    path: PathBuf,
) -> PathBuf {
    match ty {
        DepInfoPathType::PackageRootRelative => pkg_root.join(path),
        // N.B. path might be absolute here in which case the join will have no effect
        DepInfoPathType::TargetRootRelative => target_root.join(path),
    }
}

/// Some algorithms are here to ensure compatibility with possible rustc outputs.
/// The presence of an algorithm here is not a suggestion that it's fit for use.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ChecksumAlgo {
    Sha256,
    Blake3,
}

impl ChecksumAlgo {
    fn hash_len(&self) -> usize {
        match self {
            ChecksumAlgo::Sha256 | ChecksumAlgo::Blake3 => 32,
        }
    }
}

impl FromStr for ChecksumAlgo {
    type Err = InvalidChecksum;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "sha256" => Ok(Self::Sha256),
            "blake3" => Ok(Self::Blake3),
            _ => Err(InvalidChecksum::InvalidChecksumAlgo),
        }
    }
}

impl fmt::Display for ChecksumAlgo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            ChecksumAlgo::Sha256 => "sha256",
            ChecksumAlgo::Blake3 => "blake3",
        })
    }
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Checksum {
    algo: ChecksumAlgo,
    /// If the algorithm uses fewer than 32 bytes, then the remaining bytes will be zero.
    value: [u8; 32],
}

impl Checksum {
    pub fn new(algo: ChecksumAlgo, value: [u8; 32]) -> Self {
        Self { algo, value }
    }

    pub fn compute(algo: ChecksumAlgo, contents: impl Read) -> Result<Self, io::Error> {
        // Buffer size is the recommended amount to fully leverage SIMD instructions on AVX-512 as per
        // blake3 documentation.
        let mut buf = vec![0; 16 * 1024];
        let mut ret = Self {
            algo,
            value: [0; 32],
        };
        let len = algo.hash_len();
        let value = &mut ret.value[..len];

        fn digest<T>(
            mut hasher: T,
            mut update: impl FnMut(&mut T, &[u8]),
            finish: impl FnOnce(T, &mut [u8]),
            mut contents: impl Read,
            buf: &mut [u8],
            value: &mut [u8],
        ) -> Result<(), io::Error> {
            loop {
                let bytes_read = contents.read(buf)?;
                if bytes_read == 0 {
                    break;
                }
                update(&mut hasher, &buf[0..bytes_read]);
            }
            finish(hasher, value);
            Ok(())
        }

        match algo {
            ChecksumAlgo::Sha256 => {
                digest(
                    Sha256::new(),
                    |h, b| {
                        h.update(b);
                    },
                    |mut h, out| out.copy_from_slice(&h.finish()),
                    contents,
                    &mut buf,
                    value,
                )?;
            }
            ChecksumAlgo::Blake3 => {
                digest(
                    blake3::Hasher::new(),
                    |h, b| {
                        h.update(b);
                    },
                    |h, out| out.copy_from_slice(h.finalize().as_bytes()),
                    contents,
                    &mut buf,
                    value,
                )?;
            }
        }
        Ok(ret)
    }

    pub fn algo(&self) -> ChecksumAlgo {
        self.algo
    }

    pub fn value(&self) -> &[u8; 32] {
        &self.value
    }
}

impl FromStr for Checksum {
    type Err = InvalidChecksum;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut parts = s.split('=');
        let Some(algo) = parts.next().map(ChecksumAlgo::from_str).transpose()? else {
            return Err(InvalidChecksum::InvalidFormat);
        };
        let Some(checksum) = parts.next() else {
            return Err(InvalidChecksum::InvalidFormat);
        };
        let mut value = [0; 32];
        if hex::decode_to_slice(checksum, &mut value[0..algo.hash_len()]).is_err() {
            return Err(InvalidChecksum::InvalidChecksum(algo));
        }
        Ok(Self { algo, value })
    }
}

impl fmt::Display for Checksum {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut checksum = [0; 64];
        let hash_len = self.algo.hash_len();
        hex::encode_to_slice(&self.value[0..hash_len], &mut checksum[0..(hash_len * 2)])
            .map_err(|_| fmt::Error)?;
        write!(
            f,
            "{}={}",
            self.algo,
            str::from_utf8(&checksum[0..(hash_len * 2)]).unwrap_or_default()
        )
    }
}

#[derive(Debug, thiserror::Error)]
pub enum InvalidChecksum {
    #[error("algorithm portion incorrect, expected `sha256`, or `blake3`")]
    InvalidChecksumAlgo,
    #[error("expected {} hexadecimal digits in checksum portion", .0.hash_len() * 2)]
    InvalidChecksum(ChecksumAlgo),
    #[error("expected a string with format \"algorithm=hex_checksum\"")]
    InvalidFormat,
}

#[cfg(test)]
mod encoded_dep_info {
    use super::*;

    #[track_caller]
    fn gen_test(checksum: bool) {
        let checksum = checksum.then_some((768, "c01efc669f09508b55eced32d3c88702578a7c3e".into()));
        let lib_rs = (
            DepInfoPathType::TargetRootRelative,
            PathBuf::from("src/lib.rs"),
            checksum.clone(),
        );

        let depinfo = EncodedDepInfo {
            files: vec![lib_rs.clone()],
            env: Vec::new(),
        };
        let data = depinfo.serialize().unwrap();
        assert_eq!(EncodedDepInfo::parse(&data).unwrap(), depinfo);

        let mod_rs = (
            DepInfoPathType::TargetRootRelative,
            PathBuf::from("src/mod.rs"),
            checksum.clone(),
        );
        let depinfo = EncodedDepInfo {
            files: vec![lib_rs.clone(), mod_rs.clone()],
            env: Vec::new(),
        };
        let data = depinfo.serialize().unwrap();
        assert_eq!(EncodedDepInfo::parse(&data).unwrap(), depinfo);

        let depinfo = EncodedDepInfo {
            files: vec![lib_rs, mod_rs],
            env: vec![
                ("Gimli".into(), Some("Legolas".into())),
                ("Beren".into(), Some("LĂșthien".into())),
            ],
        };
        let data = depinfo.serialize().unwrap();
        assert_eq!(EncodedDepInfo::parse(&data).unwrap(), depinfo);
    }

    #[test]
    fn round_trip() {
        gen_test(false);
    }

    #[test]
    fn round_trip_with_checksums() {
        gen_test(true);
    }

    #[test]
    fn path_type_is_u8_max() {
        #[rustfmt::skip]
        let data = [
            0x01, 0x00, 0x00, 0x00, 0xff,       // magic marker
            CURRENT_ENCODED_DEP_INFO_VERSION,   // version
            0x01, 0x00, 0x00, 0x00,             // # of files
            0x00,                               // path type
            0x04, 0x00, 0x00, 0x00,             // len of path
            0x72, 0x75, 0x73, 0x74,             // path bytes ("rust")
            0x00,                               // cksum exists?
            0x00, 0x00, 0x00, 0x00,             // # of env vars
        ];
        // The current cargo doesn't recognize the magic marker.
        assert_eq!(
            EncodedDepInfo::parse(&data).unwrap(),
            EncodedDepInfo {
                files: vec![(DepInfoPathType::PackageRootRelative, "rust".into(), None)],
                env: Vec::new(),
            }
        );
    }

    #[test]
    fn parse_v0_fingerprint_dep_info() {
        #[rustfmt::skip]
        let data = [
            0x01, 0x00, 0x00, 0x00, // # of files
            0x00,                   // path type
            0x04, 0x00, 0x00, 0x00, // len of path
            0x72, 0x75, 0x73, 0x74, // path bytes: "rust"
            0x00, 0x00, 0x00, 0x00, // # of env vars
        ];
        // Cargo can't recognize v0 after `-Zchecksum-freshess` added.
        assert!(EncodedDepInfo::parse(&data).is_none());
    }
}