Skip to main content

cargo/compiler/fingerprint/
dep_info.rs

1//! Types and functions managing dep-info files.
2//! For more, see [the documentation] in the `fingerprint` module.
3//!
4//! [the documentation]: crate::compiler::fingerprint#dep-info-files
5
6use crate::util::data_structures::HashMap;
7use std::ffi::OsString;
8use std::fmt;
9use std::io;
10use std::io::Read;
11use std::path::Path;
12use std::path::PathBuf;
13use std::str;
14use std::str::FromStr;
15use std::sync::Arc;
16
17use anyhow::bail;
18use cargo_util::ProcessBuilder;
19use cargo_util::Sha256;
20use cargo_util::paths;
21use serde::Serialize;
22
23use crate::CARGO_ENV;
24use crate::CargoResult;
25use crate::workspace::manifest::ManifestMetadata;
26
27/// The current format version of [`EncodedDepInfo`].
28const CURRENT_ENCODED_DEP_INFO_VERSION: u8 = 1;
29
30/// The representation of the `.d` dep-info file generated by rustc
31#[derive(Default)]
32pub struct RustcDepInfo {
33    /// The list of files that the main target in the dep-info file depends on.
34    ///
35    /// The optional checksums are parsed from the special `# checksum:...` comments.
36    pub files: HashMap<PathBuf, Option<(u64, Checksum)>>,
37    /// The list of environment variables we found that the rustc compilation
38    /// depends on.
39    ///
40    /// The first element of the pair is the name of the env var and the second
41    /// item is the value. `Some` means that the env var was set, and `None`
42    /// means that the env var wasn't actually set and the compilation depends
43    /// on it not being set.
44    ///
45    /// These are from the special `# env-var:...` comments.
46    pub env: Vec<(String, Option<String>)>,
47}
48
49/// Tells the associated path in [`EncodedDepInfo::files`] is relative to package root,
50/// target root, or absolute.
51#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
52pub enum DepInfoPathType {
53    /// src/, e.g. src/lib.rs
54    PackageRootRelative,
55    /// {build-dir}/debug/deps/lib...
56    /// or an absolute path /.../sysroot/...
57    BuildRootRelative,
58    /// Relative to the workspace root (but not in the package root)
59    WorkspaceRootRelative,
60}
61
62/// Same as [`RustcDepInfo`] except avoids absolute paths as much as possible to
63/// allow moving around the target directory.
64///
65/// This is also stored in an optimized format to make parsing it fast because
66/// Cargo will read it for crates on all future compilations.
67///
68/// Currently the format looks like:
69///
70/// ```text
71/// +--------+---------+------------+------------+---------------+---------------+
72/// | marker | version | # of files | file paths | # of env vars | env var pairs |
73/// +--------+---------+------------+------------+---------------+---------------+
74/// ```
75///
76/// Each field represents
77///
78/// * _Marker_ --- A magic marker to ensure that older Cargoes, which only
79///   recognize format v0 (prior to checksum support in [`f4ca7390`]), do not
80///   proceed with parsing newer formats. Since [`EncodedDepInfo`] is merely
81///   an optimization, and to avoid adding complexity, Cargo recognizes only
82///   one version of [`CURRENT_ENCODED_DEP_INFO_VERSION`].
83///   The current layout looks like this
84///   ```text
85///   +----------------------------+
86///   | [0x01 0x00 0x00 0x00 0xff] |
87///   +----------------------------+
88///   ```
89///   These bytes will be interpreted as "one file tracked and an invalid
90///   [`DepInfoPathType`] variant with 255" by older Cargoes, causing them to
91///   stop parsing. This could prevent problematic parsing as noted in
92///   rust-lang/cargo#14712.
93/// * _Version_ --- The current format version.
94/// * _Number of files/envs_ --- A `u32` representing the number of things.
95/// * _File paths_ --- Zero or more paths of files the dep-info file depends on.
96///   Each path is encoded as the following:
97///
98///   ```text
99///   +-----------+-------------+------------+---------------+-----------+-------+
100///   | path type | len of path | path bytes | cksum exists? | file size | cksum |
101///   +-----------+-------------+------------+---------------+-----------+-------+
102///   ```
103/// * _Env var pairs_ --- Zero or more env vars the dep-info file depends on.
104///   Each env key-value pair is encoded as the following:
105///   ```text
106///   +------------+-----------+---------------+--------------+-------------+
107///   | len of key | key bytes | value exists? | len of value | value bytes |
108///   +------------+-----------+---------------+--------------+-------------+
109///   ```
110///
111/// [`f4ca7390`]: https://github.com/rust-lang/cargo/commit/f4ca739073185ea5e1148ff100bb4a06d3bf721d
112#[derive(Default, Debug, PartialEq, Eq)]
113pub struct EncodedDepInfo {
114    pub files: Vec<(DepInfoPathType, PathBuf, Option<(u64, String)>)>,
115    pub env: Vec<(String, Option<String>)>,
116}
117
118impl EncodedDepInfo {
119    pub fn parse(mut bytes: &[u8]) -> Option<EncodedDepInfo> {
120        let bytes = &mut bytes;
121        read_magic_marker(bytes)?;
122        let version = read_u8(bytes)?;
123        if version != CURRENT_ENCODED_DEP_INFO_VERSION {
124            return None;
125        }
126
127        let nfiles = read_usize(bytes)?;
128        let mut files = Vec::with_capacity(nfiles);
129        for _ in 0..nfiles {
130            let ty = match read_u8(bytes)? {
131                0 => DepInfoPathType::PackageRootRelative,
132                1 => DepInfoPathType::BuildRootRelative,
133                2 => DepInfoPathType::WorkspaceRootRelative,
134                _ => return None,
135            };
136            let path_bytes = read_bytes(bytes)?;
137            let path = paths::bytes2path(path_bytes).ok()?;
138            let has_checksum = read_bool(bytes)?;
139            let checksum_info = has_checksum
140                .then(|| {
141                    let file_len = read_u64(bytes);
142                    let checksum_string = read_bytes(bytes)
143                        .map(Vec::from)
144                        .and_then(|v| String::from_utf8(v).ok());
145                    file_len.zip(checksum_string)
146                })
147                .flatten();
148            files.push((ty, path, checksum_info));
149        }
150
151        let nenv = read_usize(bytes)?;
152        let mut env = Vec::with_capacity(nenv);
153        for _ in 0..nenv {
154            let key = str::from_utf8(read_bytes(bytes)?).ok()?.to_string();
155            let val = match read_u8(bytes)? {
156                0 => None,
157                1 => Some(str::from_utf8(read_bytes(bytes)?).ok()?.to_string()),
158                _ => return None,
159            };
160            env.push((key, val));
161        }
162        return Some(EncodedDepInfo { files, env });
163
164        /// See [`EncodedDepInfo`] for why a magic marker exists.
165        fn read_magic_marker(bytes: &mut &[u8]) -> Option<()> {
166            let _size = read_usize(bytes)?;
167            let path_type = read_u8(bytes)?;
168            if path_type != u8::MAX {
169                // Old depinfo. Give up parsing it.
170                None
171            } else {
172                Some(())
173            }
174        }
175
176        fn read_usize(bytes: &mut &[u8]) -> Option<usize> {
177            let ret = bytes.get(..4)?;
178            *bytes = &bytes[4..];
179            Some(u32::from_le_bytes(ret.try_into().unwrap()) as usize)
180        }
181
182        fn read_u64(bytes: &mut &[u8]) -> Option<u64> {
183            let ret = bytes.get(..8)?;
184            *bytes = &bytes[8..];
185            Some(u64::from_le_bytes(ret.try_into().unwrap()))
186        }
187
188        fn read_bool(bytes: &mut &[u8]) -> Option<bool> {
189            read_u8(bytes).map(|b| b != 0)
190        }
191
192        fn read_u8(bytes: &mut &[u8]) -> Option<u8> {
193            let ret = *bytes.get(0)?;
194            *bytes = &bytes[1..];
195            Some(ret)
196        }
197
198        fn read_bytes<'a>(bytes: &mut &'a [u8]) -> Option<&'a [u8]> {
199            let n = read_usize(bytes)? as usize;
200            let ret = bytes.get(..n)?;
201            *bytes = &bytes[n..];
202            Some(ret)
203        }
204    }
205
206    pub fn serialize(&self) -> CargoResult<Vec<u8>> {
207        let mut ret = Vec::new();
208        let dst = &mut ret;
209
210        write_magic_marker(dst);
211        dst.push(CURRENT_ENCODED_DEP_INFO_VERSION);
212
213        write_usize(dst, self.files.len());
214        for (ty, file, checksum_info) in self.files.iter() {
215            match ty {
216                DepInfoPathType::PackageRootRelative => dst.push(0),
217                DepInfoPathType::BuildRootRelative => dst.push(1),
218                DepInfoPathType::WorkspaceRootRelative => dst.push(2),
219            }
220            write_bytes(dst, paths::path2bytes(file)?);
221            write_bool(dst, checksum_info.is_some());
222            if let Some((len, checksum)) = checksum_info {
223                write_u64(dst, *len);
224                write_bytes(dst, checksum);
225            }
226        }
227
228        write_usize(dst, self.env.len());
229        for (key, val) in self.env.iter() {
230            write_bytes(dst, key);
231            match val {
232                None => dst.push(0),
233                Some(val) => {
234                    dst.push(1);
235                    write_bytes(dst, val);
236                }
237            }
238        }
239        return Ok(ret);
240
241        /// See [`EncodedDepInfo`] for why a magic marker exists.
242        ///
243        /// There is an assumption that there is always at least a file.
244        fn write_magic_marker(dst: &mut Vec<u8>) {
245            write_usize(dst, 1);
246            dst.push(u8::MAX);
247        }
248
249        fn write_bytes(dst: &mut Vec<u8>, val: impl AsRef<[u8]>) {
250            let val = val.as_ref();
251            write_usize(dst, val.len());
252            dst.extend_from_slice(val);
253        }
254
255        fn write_usize(dst: &mut Vec<u8>, val: usize) {
256            dst.extend(&u32::to_le_bytes(val as u32));
257        }
258
259        fn write_u64(dst: &mut Vec<u8>, val: u64) {
260            dst.extend(&u64::to_le_bytes(val));
261        }
262
263        fn write_bool(dst: &mut Vec<u8>, val: bool) {
264            dst.push(u8::from(val));
265        }
266    }
267}
268
269/// Parses the dep-info file coming out of rustc into a Cargo-specific format.
270///
271/// This function will parse `rustc_dep_info` as a makefile-style dep info to
272/// learn about the all files which a crate depends on. This is then
273/// re-serialized into the `cargo_dep_info` path in a Cargo-specific format.
274///
275/// The `pkg_root` argument here is the absolute path to the directory
276/// containing `Cargo.toml` for this crate that was compiled. The paths listed
277/// in the rustc dep-info file may or may not be absolute but we'll want to
278/// consider all of them relative to the `root` specified.
279///
280/// The `rustc_cwd` argument is the absolute path to the cwd of the compiler
281/// when it was invoked.
282///
283/// If the `allow_package` argument is true, then package-relative paths are
284/// included. If it is false, then package-relative paths are skipped and
285/// ignored (typically used for registry or git dependencies where we assume
286/// the source never changes, and we don't want the cost of running `stat` on
287/// all those files). See the module-level docs for the note about
288/// `-Zbinary-dep-depinfo` for more details on why this is done.
289///
290/// The serialized Cargo format will contain a list of files, all of which are
291/// relative if they're under `root`. or absolute if they're elsewhere.
292///
293/// The `env_config` argument is a set of environment variables that are
294/// defined in `[env]` table of the `config.toml`.
295pub fn translate_dep_info(
296    rustc_dep_info: &Path,
297    cargo_dep_info: &Path,
298    rustc_cwd: &Path,
299    pkg_root: &Path,
300    build_root: &Path,
301    ws_root: &Path,
302    rustc_cmd: &ProcessBuilder,
303    allow_package: bool,
304    env_config: &Arc<HashMap<String, OsString>>,
305) -> CargoResult<()> {
306    let depinfo = parse_rustc_dep_info(rustc_dep_info)?;
307
308    let build_root = crate::util::try_canonicalize(build_root)?;
309    let pkg_root = crate::util::try_canonicalize(pkg_root)?;
310    let ws_root = crate::util::try_canonicalize(ws_root)?;
311    let package_in_workspace = pkg_root.starts_with(&ws_root);
312    let mut on_disk_info = EncodedDepInfo::default();
313    on_disk_info.env = depinfo.env;
314
315    // This is a bit of a tricky statement, but here we're *removing* the
316    // dependency on environment variables that were defined specifically for
317    // the command itself. Environment variables returned by `get_envs` includes
318    // environment variables like:
319    //
320    // * `OUT_DIR` if applicable
321    // * env vars added by a build script, if any
322    //
323    // The general idea here is that the dep info file tells us what, when
324    // changed, should cause us to rebuild the crate. These environment
325    // variables are synthesized by Cargo and/or the build script, and the
326    // intention is that their values are tracked elsewhere for whether the
327    // crate needs to be rebuilt.
328    //
329    // For example a build script says when it needs to be rerun and otherwise
330    // it's assumed to produce the same output, so we're guaranteed that env
331    // vars defined by the build script will always be the same unless the build
332    // script itself reruns, in which case the crate will rerun anyway.
333    //
334    // For things like `OUT_DIR` it's a bit sketchy for now. Most of the time
335    // that's used for code generation but this is technically buggy where if
336    // you write a binary that does `println!("{}", env!("OUT_DIR"))` we won't
337    // recompile that if you move the target directory. Hopefully that's not too
338    // bad of an issue for now...
339    //
340    // This also includes `CARGO` since if the code is explicitly wanting to
341    // know that path, it should be rebuilt if it changes. The CARGO path is
342    // not tracked elsewhere in the fingerprint.
343    //
344    // For cargo#13280, We trace env vars that are defined in the `[env]` config table.
345    on_disk_info.env.retain(|(key, _)| {
346        ManifestMetadata::should_track(key)
347            || env_config.contains_key(key)
348            || !rustc_cmd.get_envs().contains_key(key)
349            || key == CARGO_ENV
350    });
351
352    let serialize_path = |file| {
353        // The path may be absolute or relative, canonical or not. Make sure
354        // it is canonicalized so we are comparing the same kinds of paths.
355        let abs_file = rustc_cwd.join(file);
356        // If canonicalization fails, just use the abs path. There is currently
357        // a bug where --remap-path-prefix is affecting .d files, causing them
358        // to point to non-existent paths.
359        let canon_file =
360            crate::util::try_canonicalize(&abs_file).unwrap_or_else(|_| abs_file.clone());
361
362        let (ty, path) = if let Ok(stripped) = canon_file.strip_prefix(&build_root) {
363            (DepInfoPathType::BuildRootRelative, stripped)
364        } else if let Ok(stripped) = canon_file.strip_prefix(&pkg_root) {
365            if !allow_package {
366                return None;
367            }
368            (DepInfoPathType::PackageRootRelative, stripped)
369        } else if package_in_workspace && let Ok(stripped) = canon_file.strip_prefix(&ws_root) {
370            (DepInfoPathType::WorkspaceRootRelative, stripped)
371        } else {
372            // It's definitely not target root relative, but this is an absolute path (since it was
373            // joined to rustc_cwd) and as such re-joining it later to the target root will have no
374            // effect.
375            (DepInfoPathType::BuildRootRelative, &*abs_file)
376        };
377        Some((ty, path.to_owned()))
378    };
379
380    for (file, checksum_info) in depinfo.files {
381        let Some((path_type, path)) = serialize_path(file) else {
382            continue;
383        };
384        on_disk_info.files.push((
385            path_type,
386            path,
387            checksum_info.map(|(len, checksum)| (len, checksum.to_string())),
388        ));
389    }
390    paths::write(cargo_dep_info, on_disk_info.serialize()?)?;
391    Ok(())
392}
393
394/// Parse the `.d` dep-info file generated by rustc.
395pub fn parse_rustc_dep_info(rustc_dep_info: &Path) -> CargoResult<RustcDepInfo> {
396    let contents = paths::read(rustc_dep_info)?;
397    let mut ret = RustcDepInfo::default();
398    let mut found_deps = false;
399
400    for line in contents.lines() {
401        if let Some(rest) = line.strip_prefix("# env-dep:") {
402            let mut parts = rest.splitn(2, '=');
403            let Some(env_var) = parts.next() else {
404                continue;
405            };
406            let env_val = match parts.next() {
407                Some(s) => Some(unescape_env(s)?),
408                None => None,
409            };
410            ret.env.push((unescape_env(env_var)?, env_val));
411        } else if let Some(pos) = line.find(": ") {
412            if found_deps {
413                continue;
414            }
415            found_deps = true;
416            let mut deps = line[pos + 2..].split_whitespace();
417
418            while let Some(s) = deps.next() {
419                let mut file = s.to_string();
420                while file.ends_with('\\') {
421                    file.pop();
422                    file.push(' ');
423                    file.push_str(deps.next().ok_or_else(|| {
424                        crate::util::internal("malformed dep-info format, trailing \\")
425                    })?);
426                }
427                ret.files.entry(file.into()).or_default();
428            }
429        } else if let Some(rest) = line.strip_prefix("# checksum:") {
430            let mut parts = rest.splitn(3, ' ');
431            let Some(checksum) = parts.next().map(Checksum::from_str).transpose()? else {
432                continue;
433            };
434            let Some(Ok(file_len)) = parts
435                .next()
436                .and_then(|s| s.strip_prefix("file_len:").map(|s| s.parse::<u64>()))
437            else {
438                continue;
439            };
440            let Some(path) = parts.next().map(PathBuf::from) else {
441                continue;
442            };
443
444            ret.files.insert(path, Some((file_len, checksum)));
445        }
446    }
447    return Ok(ret);
448
449    // rustc tries to fit env var names and values all on a single line, which
450    // means it needs to escape `\r` and `\n`. The escape syntax used is "\n"
451    // which means that `\` also needs to be escaped.
452    fn unescape_env(s: &str) -> CargoResult<String> {
453        let mut ret = String::with_capacity(s.len());
454        let mut chars = s.chars();
455        while let Some(c) = chars.next() {
456            if c != '\\' {
457                ret.push(c);
458                continue;
459            }
460            match chars.next() {
461                Some('\\') => ret.push('\\'),
462                Some('n') => ret.push('\n'),
463                Some('r') => ret.push('\r'),
464                Some(c) => bail!("unknown escape character `{}`", c),
465                None => bail!("unterminated escape character"),
466            }
467        }
468        Ok(ret)
469    }
470}
471
472/// Parses Cargo's internal [`EncodedDepInfo`] structure that was previously
473/// serialized to disk.
474///
475/// Note that this is not rustc's `*.d` files.
476///
477/// Also note that rustc's `*.d` files are translated to Cargo-specific
478/// `EncodedDepInfo` files after compilations have finished in
479/// [`translate_dep_info`].
480///
481/// Returns `None` if the file is corrupt or couldn't be read from disk. This
482/// indicates that the crate should likely be rebuilt.
483pub fn parse_dep_info(
484    pkg_root: &Path,
485    build_root: &Path,
486    ws_root: &Path,
487    dep_info: &Path,
488) -> CargoResult<Option<RustcDepInfo>> {
489    let Ok(data) = paths::read_bytes(dep_info) else {
490        return Ok(None);
491    };
492    let Some(info) = EncodedDepInfo::parse(&data) else {
493        tracing::warn!("failed to parse cargo's dep-info at {:?}", dep_info);
494        return Ok(None);
495    };
496    let mut ret = RustcDepInfo::default();
497    ret.env = info.env;
498    ret.files
499        .extend(info.files.into_iter().map(|(ty, path, checksum_info)| {
500            (
501                make_absolute_path(ty, pkg_root, build_root, ws_root, path),
502                checksum_info.and_then(|(file_len, checksum)| {
503                    Checksum::from_str(&checksum).ok().map(|c| (file_len, c))
504                }),
505            )
506        }));
507    Ok(Some(ret))
508}
509
510fn make_absolute_path(
511    ty: DepInfoPathType,
512    pkg_root: &Path,
513    build_root: &Path,
514    ws_root: &Path,
515    path: PathBuf,
516) -> PathBuf {
517    let relative_to = match ty {
518        DepInfoPathType::PackageRootRelative => pkg_root,
519        // N.B. path might be absolute here in which case the join below will have no effect
520        DepInfoPathType::BuildRootRelative => build_root,
521        DepInfoPathType::WorkspaceRootRelative => ws_root,
522    };
523
524    if path.as_os_str().is_empty() {
525        // Joining with an empty path causes Rust to add a trailing path separator. On Windows, this
526        // would add an invalid trailing backslash to the .d file.
527        return relative_to.to_path_buf();
528    }
529
530    relative_to.join(path)
531}
532
533/// Some algorithms are here to ensure compatibility with possible rustc outputs.
534/// The presence of an algorithm here is not a suggestion that it's fit for use.
535#[derive(Copy, Clone, Debug, Eq, PartialEq)]
536pub enum ChecksumAlgo {
537    Sha256,
538    Blake3,
539}
540
541impl ChecksumAlgo {
542    fn hash_len(&self) -> usize {
543        match self {
544            ChecksumAlgo::Sha256 | ChecksumAlgo::Blake3 => 32,
545        }
546    }
547}
548
549impl FromStr for ChecksumAlgo {
550    type Err = InvalidChecksum;
551
552    fn from_str(s: &str) -> Result<Self, Self::Err> {
553        match s {
554            "sha256" => Ok(Self::Sha256),
555            "blake3" => Ok(Self::Blake3),
556            _ => Err(InvalidChecksum::InvalidChecksumAlgo),
557        }
558    }
559}
560
561impl fmt::Display for ChecksumAlgo {
562    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563        f.write_str(match self {
564            ChecksumAlgo::Sha256 => "sha256",
565            ChecksumAlgo::Blake3 => "blake3",
566        })
567    }
568}
569
570#[derive(Copy, Clone, Debug, Eq, PartialEq)]
571pub struct Checksum {
572    algo: ChecksumAlgo,
573    /// If the algorithm uses fewer than 32 bytes, then the remaining bytes will be zero.
574    value: [u8; 32],
575}
576
577impl Checksum {
578    pub fn new(algo: ChecksumAlgo, value: [u8; 32]) -> Self {
579        Self { algo, value }
580    }
581
582    pub fn compute(algo: ChecksumAlgo, contents: impl Read) -> Result<Self, io::Error> {
583        // Buffer size is the recommended amount to fully leverage SIMD instructions on AVX-512 as per
584        // blake3 documentation.
585        let mut buf = vec![0; 16 * 1024];
586        let mut ret = Self {
587            algo,
588            value: [0; 32],
589        };
590        let len = algo.hash_len();
591        let value = &mut ret.value[..len];
592
593        fn digest<T>(
594            mut hasher: T,
595            mut update: impl FnMut(&mut T, &[u8]),
596            finish: impl FnOnce(T, &mut [u8]),
597            mut contents: impl Read,
598            buf: &mut [u8],
599            value: &mut [u8],
600        ) -> Result<(), io::Error> {
601            loop {
602                let bytes_read = contents.read(buf)?;
603                if bytes_read == 0 {
604                    break;
605                }
606                update(&mut hasher, &buf[0..bytes_read]);
607            }
608            finish(hasher, value);
609            Ok(())
610        }
611
612        match algo {
613            ChecksumAlgo::Sha256 => {
614                digest(
615                    Sha256::new(),
616                    |h, b| {
617                        h.update(b);
618                    },
619                    |mut h, out| out.copy_from_slice(&h.finish()),
620                    contents,
621                    &mut buf,
622                    value,
623                )?;
624            }
625            ChecksumAlgo::Blake3 => {
626                digest(
627                    blake3::Hasher::new(),
628                    |h, b| {
629                        h.update(b);
630                    },
631                    |h, out| out.copy_from_slice(h.finalize().as_bytes()),
632                    contents,
633                    &mut buf,
634                    value,
635                )?;
636            }
637        }
638        Ok(ret)
639    }
640
641    pub fn algo(&self) -> ChecksumAlgo {
642        self.algo
643    }
644
645    pub fn value(&self) -> &[u8; 32] {
646        &self.value
647    }
648}
649
650impl FromStr for Checksum {
651    type Err = InvalidChecksum;
652
653    fn from_str(s: &str) -> Result<Self, Self::Err> {
654        let mut parts = s.split('=');
655        let Some(algo) = parts.next().map(ChecksumAlgo::from_str).transpose()? else {
656            return Err(InvalidChecksum::InvalidFormat);
657        };
658        let Some(checksum) = parts.next() else {
659            return Err(InvalidChecksum::InvalidFormat);
660        };
661        let mut value = [0; 32];
662        if hex::decode_to_slice(checksum, &mut value[0..algo.hash_len()]).is_err() {
663            return Err(InvalidChecksum::InvalidChecksum(algo));
664        }
665        Ok(Self { algo, value })
666    }
667}
668
669impl fmt::Display for Checksum {
670    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671        let mut checksum = [0; 64];
672        let hash_len = self.algo.hash_len();
673        hex::encode_to_slice(&self.value[0..hash_len], &mut checksum[0..(hash_len * 2)])
674            .map_err(|_| fmt::Error)?;
675        write!(
676            f,
677            "{}={}",
678            self.algo,
679            str::from_utf8(&checksum[0..(hash_len * 2)]).unwrap_or_default()
680        )
681    }
682}
683
684impl Serialize for Checksum {
685    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
686    where
687        S: serde::Serializer,
688    {
689        serializer.serialize_str(&self.to_string())
690    }
691}
692
693impl<'de> serde::Deserialize<'de> for Checksum {
694    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
695    where
696        D: serde::Deserializer<'de>,
697    {
698        let s = String::deserialize(deserializer)?;
699        s.parse().map_err(serde::de::Error::custom)
700    }
701}
702
703#[derive(Debug, thiserror::Error)]
704pub enum InvalidChecksum {
705    #[error("algorithm portion incorrect, expected `sha256`, or `blake3`")]
706    InvalidChecksumAlgo,
707    #[error("expected {} hexadecimal digits in checksum portion", .0.hash_len() * 2)]
708    InvalidChecksum(ChecksumAlgo),
709    #[error("expected a string with format \"algorithm=hex_checksum\"")]
710    InvalidFormat,
711}
712
713#[cfg(test)]
714mod encoded_dep_info {
715    use super::*;
716
717    #[track_caller]
718    fn gen_test(checksum: bool) {
719        let checksum = checksum.then_some((768, "c01efc669f09508b55eced32d3c88702578a7c3e".into()));
720        let lib_rs = (
721            DepInfoPathType::BuildRootRelative,
722            PathBuf::from("src/lib.rs"),
723            checksum.clone(),
724        );
725
726        let depinfo = EncodedDepInfo {
727            files: vec![lib_rs.clone()],
728            env: Vec::new(),
729        };
730        let data = depinfo.serialize().unwrap();
731        assert_eq!(EncodedDepInfo::parse(&data).unwrap(), depinfo);
732
733        let mod_rs = (
734            DepInfoPathType::BuildRootRelative,
735            PathBuf::from("src/mod.rs"),
736            checksum.clone(),
737        );
738        let depinfo = EncodedDepInfo {
739            files: vec![lib_rs.clone(), mod_rs.clone()],
740            env: Vec::new(),
741        };
742        let data = depinfo.serialize().unwrap();
743        assert_eq!(EncodedDepInfo::parse(&data).unwrap(), depinfo);
744
745        let depinfo = EncodedDepInfo {
746            files: vec![lib_rs, mod_rs],
747            env: vec![
748                ("Gimli".into(), Some("Legolas".into())),
749                ("Beren".into(), Some("LĂșthien".into())),
750            ],
751        };
752        let data = depinfo.serialize().unwrap();
753        assert_eq!(EncodedDepInfo::parse(&data).unwrap(), depinfo);
754    }
755
756    #[test]
757    fn round_trip() {
758        gen_test(false);
759    }
760
761    #[test]
762    fn round_trip_with_checksums() {
763        gen_test(true);
764    }
765
766    #[test]
767    fn path_type_is_u8_max() {
768        #[rustfmt::skip]
769        let data = [
770            0x01, 0x00, 0x00, 0x00, 0xff,       // magic marker
771            CURRENT_ENCODED_DEP_INFO_VERSION,   // version
772            0x01, 0x00, 0x00, 0x00,             // # of files
773            0x00,                               // path type
774            0x04, 0x00, 0x00, 0x00,             // len of path
775            0x72, 0x75, 0x73, 0x74,             // path bytes ("rust")
776            0x00,                               // cksum exists?
777            0x00, 0x00, 0x00, 0x00,             // # of env vars
778        ];
779        // The current cargo doesn't recognize the magic marker.
780        assert_eq!(
781            EncodedDepInfo::parse(&data).unwrap(),
782            EncodedDepInfo {
783                files: vec![(DepInfoPathType::PackageRootRelative, "rust".into(), None)],
784                env: Vec::new(),
785            }
786        );
787    }
788
789    #[test]
790    fn parse_v0_fingerprint_dep_info() {
791        #[rustfmt::skip]
792        let data = [
793            0x01, 0x00, 0x00, 0x00, // # of files
794            0x00,                   // path type
795            0x04, 0x00, 0x00, 0x00, // len of path
796            0x72, 0x75, 0x73, 0x74, // path bytes: "rust"
797            0x00, 0x00, 0x00, 0x00, // # of env vars
798        ];
799        // Cargo can't recognize v0 after `-Zchecksum-freshness` added.
800        assert!(EncodedDepInfo::parse(&data).is_none());
801    }
802}