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