#
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;
    21
    22use crate::CARGO_ENV;
    23use crate::CargoResult;
    24use crate::core::manifest::ManifestMetadata;
    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    /// {build-dir}/debug/deps/lib...
    55    /// or an absolute path /.../sysroot/...
    56    BuildRootRelative,
    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::BuildRootRelative,
    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::BuildRootRelative => 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    build_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 build_root = crate::util::try_canonicalize(build_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(&build_root) {
    355            (DepInfoPathType::BuildRootRelative, 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::BuildRootRelative, &*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    build_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, build_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    build_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::BuildRootRelative => build_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::BuildRootRelative,
    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::BuildRootRelative,
    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-freshness` added.
    760        assert!(EncodedDepInfo::parse(&data).is_none());
    761    }
    762}