#
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}