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