#
cargo/core/resolver/
encode.rs

    1//! Definition of how to encode a `Resolve` into a TOML `Cargo.lock` file
    2//!
    3//! This module contains all machinery necessary to parse a `Resolve` from a
    4//! `Cargo.lock` as well as serialize a `Resolve` to a `Cargo.lock`.
    5//!
    6//! ## Changing `Cargo.lock`
    7//!
    8//! In general Cargo is quite conservative about changing the format of
    9//! `Cargo.lock`. Usage of new features in Cargo can change `Cargo.lock` at any
    10//! time, but otherwise changing the serialization of `Cargo.lock` is a
    11//! difficult operation to do that we typically avoid.
    12//!
    13//! The main problem with changing the format of `Cargo.lock` is that it can
    14//! cause quite a bad experience for end users who use different versions of
    15//! Cargo. If every PR to a project oscillates between the stable channel's
    16//! encoding of Cargo.lock and the nightly channel's encoding then that's a
    17//! pretty bad experience.
    18//!
    19//! We do, however, want to change `Cargo.lock` over time. (and we have!). To do
    20//! this the rules that we currently have are:
    21//!
    22//! * Add support for the new format to Cargo. This involves code changes in
    23//!   Cargo itself, likely by adding a new variant of `ResolveVersion` and
    24//!   branching on that where necessary. This is accompanied with tests in the
    25//!   `lockfile_compat` module.
    26//!
    27//!   * Do not update `ResolveVersion::default()`. The new lockfile format will
    28//!     not be used yet.
    29//!
    30//!   * Preserve the new format if found. This means that if Cargo finds the new
    31//!     version it'll keep using it, but otherwise it continues to use whatever
    32//!     format it previously found.
    33//!
    34//! * Wait a "long time". This is at least until the changes here hit stable
    35//!   Rust. Often though we wait a little longer to let the changes percolate
    36//!   into one or two older stable releases.
    37//!
    38//! * Change the return value of `ResolveVersion::default()` to the new format.
    39//!   This will cause new lock files to use the latest encoding as well as
    40//!   causing any operation which updates the lock file to update to the new
    41//!   format.
    42//!
    43//! This migration scheme in general means that Cargo we'll get *support* for a
    44//! new format into Cargo ASAP, but it won't be exercised yet (except in Cargo's
    45//! own tests). Eventually when stable/beta/nightly all have support for the new
    46//! format (and maybe a few previous stable versions) we flip the switch.
    47//! Projects on nightly will quickly start seeing changes, but
    48//! stable/beta/nightly will all understand this new format and will preserve
    49//! it.
    50//!
    51//! While this does mean that projects' `Cargo.lock` changes over time, it's
    52//! typically a pretty minimal effort change that's just "check in what's
    53//! there".
    54//!
    55//! ## Historical changes to `Cargo.lock`
    56//!
    57//! Listed from most recent to oldest, these are some of the changes we've made
    58//! to `Cargo.lock`'s serialization format:
    59//!
    60//! * A `version` marker is now at the top of the lock file which is a way for
    61//!   super-old Cargos (at least since this was implemented) to give a formal
    62//!   error if they see a lock file from a super-future Cargo. Additionally as
    63//!   part of this change the encoding of `git` dependencies in lock files
    64//!   changed where `branch = "master"` is now encoded with `branch=master`
    65//!   instead of with nothing at all.
    66//!
    67//! * The entries in `dependencies` arrays have been shortened and the
    68//!   `checksum` field now shows up directly in `[[package]]` instead of always
    69//!   at the end of the file. The goal of this change was to ideally reduce
    70//!   merge conflicts being generated on `Cargo.lock`. Updating a version of a
    71//!   package now only updates two lines in the file, the checksum and the
    72//!   version number, most of the time. Dependency edges are specified in a
    73//!   compact form where possible where just the name is listed. The
    74//!   version/source on dependency edges are only listed if necessary to
    75//!   disambiguate which version or which source is in use.
    76//!
    77//! * A comment at the top of the file indicates that the file is a generated
    78//!   file and contains the special symbol `@generated` to indicate to common
    79//!   review tools that it's a generated file.
    80//!
    81//! * A `[root]` entry for the "root crate" has been removed and instead now
    82//!   included in `[[package]]` like everything else.
    83//!
    84//! * All packages from registries contain a `checksum` which is a sha256
    85//!   checksum of the tarball the package is associated with. This is all stored
    86//!   in the `[metadata]` table of `Cargo.lock` which all versions of Cargo
    87//!   since 1.0 have preserved. The goal of this was to start recording
    88//!   checksums so mirror sources can be verified.
    89//!
    90//! ## Other oddities about `Cargo.lock`
    91//!
    92//! There's a few other miscellaneous weird things about `Cargo.lock` that you
    93//! may want to be aware of when reading this file:
    94//!
    95//! * All packages have a `source` listed to indicate where they come from. For
    96//!   `path` dependencies, however, no `source` is listed. There's no way we
    97//!   could emit a filesystem path name and have that be portable across
    98//!   systems, so all packages from a `path` are not listed with a `source`.
    99//!   Note that this also means that all packages with `path` sources must have
    100//!   unique names.
    101//!
    102//! * The `[metadata]` table in `Cargo.lock` is intended to be a generic mapping
    103//!   of strings to strings that's simply preserved by Cargo. This was a very
    104//!   early effort to be forward compatible against changes to `Cargo.lock`'s
    105//!   format. This is nowadays sort of deemed a bad idea though and we don't
    106//!   really use it that much except for `checksum`s historically. It's not
    107//!   really recommended to use this.
    108//!
    109//! * The actual literal on-disk serialization is found in
    110//!   `src/cargo/ops/lockfile.rs` which basically renders a `toml::Value` in a
    111//!   special fashion to make sure we have strict control over the on-disk
    112//!   format.
    113
    114use super::{Resolve, ResolveVersion};
    115use crate::core::{Dependency, GitReference, Package, PackageId, SourceId, Workspace};
    116use crate::util::errors::CargoResult;
    117use crate::util::interning::InternedString;
    118use crate::util::{Graph, internal};
    119use anyhow::{Context as _, bail};
    120use serde::de;
    121use serde::ser;
    122use serde::{Deserialize, Serialize};
    123use std::collections::{BTreeMap, HashMap, HashSet};
    124use std::fmt;
    125use std::str::FromStr;
    126use tracing::debug;
    127
    128/// The `Cargo.lock` structure.
    129#[derive(Serialize, Deserialize, Debug)]
    130pub struct EncodableResolve {
    131    version: Option<u32>,
    132    package: Option<Vec<EncodableDependency>>,
    133    /// `root` is optional to allow backward compatibility.
    134    root: Option<EncodableDependency>,
    135    metadata: Option<Metadata>,
    136    #[serde(default, skip_serializing_if = "Patch::is_empty")]
    137    patch: Patch,
    138}
    139
    140#[derive(Serialize, Deserialize, Debug, Default)]
    141struct Patch {
    142    unused: Vec<EncodableDependency>,
    143}
    144
    145pub type Metadata = BTreeMap<String, String>;
    146
    147impl EncodableResolve {
    148    /// Convert a `Cargo.lock` to a Resolve.
    149    ///
    150    /// Note that this `Resolve` is not "complete". For example, the
    151    /// dependencies do not know the difference between regular/dev/build
    152    /// dependencies, so they are not filled in. It also does not include
    153    /// `features`. Care should be taken when using this Resolve. One of the
    154    /// primary uses is to be used with `resolve_with_previous` to guide the
    155    /// resolver to create a complete Resolve.
    156    pub fn into_resolve(self, original: &str, ws: &Workspace<'_>) -> CargoResult<Resolve> {
    157        let path_deps: HashMap<String, HashMap<semver::Version, SourceId>> = build_path_deps(ws)?;
    158        let mut checksums = HashMap::new();
    159
    160        let mut version = match self.version {
    161            Some(n @ 5) if ws.gctx().nightly_features_allowed => {
    162                if ws.gctx().cli_unstable().next_lockfile_bump {
    163                    ResolveVersion::V5
    164                } else {
    165                    anyhow::bail!("lock file version `{n}` requires `-Znext-lockfile-bump`");
    166                }
    167            }
    168            Some(4) => ResolveVersion::V4,
    169            Some(3) => ResolveVersion::V3,
    170            Some(n) => bail!(
    171                "lock file version `{}` was found, but this version of Cargo \
    172                 does not understand this lock file, perhaps Cargo needs \
    173                 to be updated?",
    174                n,
    175            ),
    176            // Historically Cargo did not have a version indicator in lock
    177            // files, so this could either be the V1 or V2 encoding. We assume
    178            // an older format is being parsed until we see so otherwise.
    179            None => ResolveVersion::V1,
    180        };
    181
    182        let packages = {
    183            let mut packages = self.package.unwrap_or_default();
    184            if let Some(root) = self.root {
    185                packages.insert(0, root);
    186            }
    187            packages
    188        };
    189
    190        // `PackageId`s in the lock file don't include the `source` part
    191        // for workspace members, so we reconstruct proper IDs.
    192        let live_pkgs = {
    193            let mut live_pkgs = HashMap::new();
    194            let mut all_pkgs = HashSet::new();
    195            for pkg in packages.iter() {
    196                let enc_id = EncodablePackageId {
    197                    name: pkg.name.clone(),
    198                    version: Some(pkg.version.clone()),
    199                    source: pkg.source.clone(),
    200                };
    201
    202                if !all_pkgs.insert(enc_id.clone()) {
    203                    anyhow::bail!("package `{}` is specified twice in the lockfile", pkg.name);
    204                }
    205                let id = match pkg
    206                    .source
    207                    .as_deref()
    208                    .or_else(|| get_source_id(&path_deps, pkg))
    209                {
    210                    // We failed to find a local package in the workspace.
    211                    // It must have been removed and should be ignored.
    212                    None => {
    213                        debug!("path dependency now missing {} v{}", pkg.name, pkg.version);
    214                        continue;
    215                    }
    216                    Some(&source) => PackageId::try_new(&pkg.name, &pkg.version, source)?,
    217                };
    218
    219                // If a package has a checksum listed directly on it then record
    220                // that here, and we also bump our version up to 2 since V1
    221                // didn't ever encode this field.
    222                if let Some(cksum) = &pkg.checksum {
    223                    version = version.max(ResolveVersion::V2);
    224                    checksums.insert(id, Some(cksum.clone()));
    225                }
    226
    227                assert!(live_pkgs.insert(enc_id, (id, pkg)).is_none())
    228            }
    229            live_pkgs
    230        };
    231
    232        // When decoding a V2 version the edges in `dependencies` aren't
    233        // guaranteed to have either version or source information. This `map`
    234        // is used to find package ids even if dependencies have missing
    235        // information. This map is from name to version to source to actual
    236        // package ID. (various levels to drill down step by step)
    237        let mut map = HashMap::new();
    238        for (id, _) in live_pkgs.values() {
    239            map.entry(id.name().as_str())
    240                .or_insert_with(HashMap::new)
    241                .entry(id.version().to_string())
    242                .or_insert_with(HashMap::new)
    243                .insert(id.source_id(), *id);
    244        }
    245
    246        let mut lookup_id = |enc_id: &EncodablePackageId| -> Option<PackageId> {
    247            // The name of this package should always be in the larger list of
    248            // all packages.
    249            let by_version = map.get(enc_id.name.as_str())?;
    250
    251            // If the version is provided, look that up. Otherwise if the
    252            // version isn't provided this is a V2 manifest and we should only
    253            // have one version for this name. If we have more than one version
    254            // for the name then it's ambiguous which one we'd use. That
    255            // shouldn't ever actually happen but in theory bad git merges could
    256            // produce invalid lock files, so silently ignore these cases.
    257            let by_source = match &enc_id.version {
    258                Some(version) => by_version.get(version)?,
    259                None => {
    260                    version = version.max(ResolveVersion::V2);
    261                    if by_version.len() == 1 {
    262                        by_version.values().next().unwrap()
    263                    } else {
    264                        return None;
    265                    }
    266                }
    267            };
    268
    269            // This is basically the same as above. Note though that `source` is
    270            // always missing for path dependencies regardless of serialization
    271            // format. That means we have to handle the `None` case a bit more
    272            // carefully.
    273            match &enc_id.source {
    274                Some(source) => by_source.get(source).cloned(),
    275                None => {
    276                    // Look through all possible packages ids for this
    277                    // name/version. If there's only one `path` dependency then
    278                    // we are hardcoded to use that since `path` dependencies
    279                    // can't have a source listed.
    280                    let mut path_packages = by_source.values().filter(|p| p.source_id().is_path());
    281                    if let Some(path) = path_packages.next() {
    282                        if path_packages.next().is_some() {
    283                            return None;
    284                        }
    285                        Some(*path)
    286
    287                    // ... otherwise if there's only one then we must be
    288                    // implicitly using that one due to a V2 serialization of
    289                    // the lock file
    290                    } else if by_source.len() == 1 {
    291                        let id = by_source.values().next().unwrap();
    292                        version = version.max(ResolveVersion::V2);
    293                        Some(*id)
    294
    295                    // ... and failing that we probably had a bad git merge of
    296                    // `Cargo.lock` or something like that, so just ignore this.
    297                    } else {
    298                        None
    299                    }
    300                }
    301            }
    302        };
    303
    304        let mut g = Graph::new();
    305
    306        for (id, _) in live_pkgs.values() {
    307            g.add(*id);
    308        }
    309
    310        for &(ref id, pkg) in live_pkgs.values() {
    311            let Some(ref deps) = pkg.dependencies else {
    312                continue;
    313            };
    314
    315            for edge in deps.iter() {
    316                if let Some(to_depend_on) = lookup_id(edge) {
    317                    g.link(*id, to_depend_on);
    318                }
    319            }
    320        }
    321
    322        let replacements = {
    323            let mut replacements = HashMap::new();
    324            for &(ref id, pkg) in live_pkgs.values() {
    325                if let Some(ref replace) = pkg.replace {
    326                    assert!(pkg.dependencies.is_none());
    327                    if let Some(replace_id) = lookup_id(replace) {
    328                        replacements.insert(*id, replace_id);
    329                    }
    330                }
    331            }
    332            replacements
    333        };
    334
    335        let mut metadata = self.metadata.unwrap_or_default();
    336
    337        // In the V1 serialization formats all checksums were listed in the lock
    338        // file in the `[metadata]` section, so if we're still V1 then look for
    339        // that here.
    340        let prefix = "checksum ";
    341        let mut to_remove = Vec::new();
    342        for (k, v) in metadata.iter().filter(|p| p.0.starts_with(prefix)) {
    343            to_remove.push(k.to_string());
    344            let k = k.strip_prefix(prefix).unwrap();
    345            let enc_id: EncodablePackageId = k
    346                .parse()
    347                .with_context(|| internal("invalid encoding of checksum in lockfile"))?;
    348            let Some(id) = lookup_id(&enc_id) else {
    349                continue;
    350            };
    351
    352            let v = if v == "<none>" {
    353                None
    354            } else {
    355                Some(v.to_string())
    356            };
    357            checksums.insert(id, v);
    358        }
    359        // If `checksum` was listed in `[metadata]` but we were previously
    360        // listed as `V2` then assume some sort of bad git merge happened, so
    361        // discard all checksums and let's regenerate them later.
    362        if !to_remove.is_empty() && version >= ResolveVersion::V2 {
    363            checksums.drain();
    364        }
    365        for k in to_remove {
    366            metadata.remove(&k);
    367        }
    368
    369        let mut unused_patches = Vec::new();
    370        for pkg in self.patch.unused {
    371            let id = match pkg
    372                .source
    373                .as_deref()
    374                .or_else(|| get_source_id(&path_deps, &pkg))
    375            {
    376                Some(&src) => PackageId::try_new(&pkg.name, &pkg.version, src)?,
    377                None => continue,
    378            };
    379            unused_patches.push(id);
    380        }
    381
    382        // We have a curious issue where in the "v1 format" we buggily had a
    383        // trailing blank line at the end of lock files under some specific
    384        // conditions.
    385        //
    386        // Cargo is trying to write new lockfies in the "v2 format" but if you
    387        // have no dependencies, for example, then the lockfile encoded won't
    388        // really have any indicator that it's in the new format (no
    389        // dependencies or checksums listed). This means that if you type `cargo
    390        // new` followed by `cargo build` it will generate a "v2 format" lock
    391        // file since none previously existed. When reading this on the next
    392        // `cargo build`, however, it generates a new lock file because when
    393        // reading in that lockfile we think it's the v1 format.
    394        //
    395        // To help fix this issue we special case here. If our lockfile only has
    396        // one trailing newline, not two, *and* it only has one package, then
    397        // this is actually the v2 format.
    398        if original.ends_with('\n')
    399            && !original.ends_with("\n\n")
    400            && version == ResolveVersion::V1
    401            && g.iter().count() == 1
    402        {
    403            version = ResolveVersion::V2;
    404        }
    405
    406        return Ok(Resolve::new(
    407            g,
    408            replacements,
    409            HashMap::new(),
    410            checksums,
    411            metadata,
    412            unused_patches,
    413            version,
    414            HashMap::new(),
    415        ));
    416
    417        fn get_source_id<'a>(
    418            path_deps: &'a HashMap<String, HashMap<semver::Version, SourceId>>,
    419            pkg: &'a EncodableDependency,
    420        ) -> Option<&'a SourceId> {
    421            path_deps.iter().find_map(|(name, version_source)| {
    422                if name != &pkg.name || version_source.len() == 0 {
    423                    return None;
    424                }
    425                if version_source.len() == 1 {
    426                    return Some(version_source.values().next().unwrap());
    427                }
    428                // If there are multiple candidates for the same name, it needs to be determined by combining versions (See #13405).
    429                if let Ok(pkg_version) = pkg.version.parse::<semver::Version>() {
    430                    if let Some(source_id) = version_source.get(&pkg_version) {
    431                        return Some(source_id);
    432                    }
    433                }
    434
    435                None
    436            })
    437        }
    438    }
    439}
    440
    441fn build_path_deps(
    442    ws: &Workspace<'_>,
    443) -> CargoResult<HashMap<String, HashMap<semver::Version, SourceId>>> {
    444    // If a crate is **not** a path source, then we're probably in a situation
    445    // such as `cargo install` with a lock file from a remote dependency. In
    446    // that case we don't need to fixup any path dependencies (as they're not
    447    // actually path dependencies any more), so we ignore them.
    448    let members = ws
    449        .members()
    450        .filter(|p| p.package_id().source_id().is_path())
    451        .collect::<Vec<_>>();
    452
    453    let mut ret: HashMap<String, HashMap<semver::Version, SourceId>> = HashMap::new();
    454    let mut visited = HashSet::new();
    455    for member in members.iter() {
    456        ret.entry(member.package_id().name().to_string())
    457            .or_insert_with(HashMap::new)
    458            .insert(
    459                member.package_id().version().clone(),
    460                member.package_id().source_id(),
    461            );
    462        visited.insert(member.package_id().source_id());
    463    }
    464    for member in members.iter() {
    465        build_pkg(member, ws, &mut ret, &mut visited);
    466    }
    467    for deps in ws.root_patch()?.values() {
    468        for dep in deps {
    469            build_dep(dep, ws, &mut ret, &mut visited);
    470        }
    471    }
    472    for (_, dep) in ws.root_replace() {
    473        build_dep(dep, ws, &mut ret, &mut visited);
    474    }
    475
    476    return Ok(ret);
    477
    478    fn build_pkg(
    479        pkg: &Package,
    480        ws: &Workspace<'_>,
    481        ret: &mut HashMap<String, HashMap<semver::Version, SourceId>>,
    482        visited: &mut HashSet<SourceId>,
    483    ) {
    484        for dep in pkg.dependencies() {
    485            build_dep(dep, ws, ret, visited);
    486        }
    487    }
    488
    489    fn build_dep(
    490        dep: &Dependency,
    491        ws: &Workspace<'_>,
    492        ret: &mut HashMap<String, HashMap<semver::Version, SourceId>>,
    493        visited: &mut HashSet<SourceId>,
    494    ) {
    495        let id = dep.source_id();
    496        if visited.contains(&id) || !id.is_path() {
    497            return;
    498        }
    499        let path = match id.url().to_file_path() {
    500            Ok(p) => p.join("Cargo.toml"),
    501            Err(_) => return,
    502        };
    503        let Ok(pkg) = ws.load(&path) else { return };
    504        ret.entry(pkg.package_id().name().to_string())
    505            .or_insert_with(HashMap::new)
    506            .insert(
    507                pkg.package_id().version().clone(),
    508                pkg.package_id().source_id(),
    509            );
    510        visited.insert(pkg.package_id().source_id());
    511        build_pkg(&pkg, ws, ret, visited);
    512    }
    513}
    514
    515impl Patch {
    516    fn is_empty(&self) -> bool {
    517        self.unused.is_empty()
    518    }
    519}
    520
    521#[derive(Serialize, Deserialize, Debug, PartialOrd, Ord, PartialEq, Eq)]
    522pub struct EncodableDependency {
    523    name: String,
    524    version: String,
    525    source: Option<EncodableSourceId>,
    526    checksum: Option<String>,
    527    dependencies: Option<Vec<EncodablePackageId>>,
    528    replace: Option<EncodablePackageId>,
    529}
    530
    531/// Pretty much equivalent to [`SourceId`] with a different serialization method.
    532///
    533/// The serialization for `SourceId` doesn't do URL encode for parameters.
    534/// In contrast, this type is aware of that whenever [`ResolveVersion`] allows
    535/// us to do so (v4 or later).
    536#[derive(Deserialize, Debug, PartialOrd, Ord, Clone)]
    537#[serde(transparent)]
    538pub struct EncodableSourceId {
    539    inner: SourceId,
    540    /// We don't care about the deserialization of this, as the `url` crate
    541    /// will always decode as the URL was encoded. Only when a [`Resolve`]
    542    /// turns into a [`EncodableResolve`] will it set the value accordingly
    543    /// via [`encodable_source_id`].
    544    #[serde(skip)]
    545    encoded: bool,
    546}
    547
    548impl EncodableSourceId {
    549    /// Creates a `EncodableSourceId` that always encodes URL params.
    550    fn new(inner: SourceId) -> Self {
    551        Self {
    552            inner,
    553            encoded: true,
    554        }
    555    }
    556
    557    /// Creates a `EncodableSourceId` that doesn't encode URL params. This is
    558    /// for backward compatibility for order lockfile version.
    559    fn without_url_encoded(inner: SourceId) -> Self {
    560        Self {
    561            inner,
    562            encoded: false,
    563        }
    564    }
    565
    566    /// Encodes the inner [`SourceId`] as a URL.
    567    fn as_url(&self) -> impl fmt::Display + '_ {
    568        if self.encoded {
    569            self.inner.as_encoded_url()
    570        } else {
    571            self.inner.as_url()
    572        }
    573    }
    574}
    575
    576impl std::ops::Deref for EncodableSourceId {
    577    type Target = SourceId;
    578
    579    fn deref(&self) -> &Self::Target {
    580        &self.inner
    581    }
    582}
    583
    584impl ser::Serialize for EncodableSourceId {
    585    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    586    where
    587        S: ser::Serializer,
    588    {
    589        s.collect_str(&self.as_url())
    590    }
    591}
    592
    593impl std::hash::Hash for EncodableSourceId {
    594    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    595        self.inner.hash(state)
    596    }
    597}
    598
    599impl std::cmp::PartialEq for EncodableSourceId {
    600    fn eq(&self, other: &Self) -> bool {
    601        self.inner == other.inner
    602    }
    603}
    604
    605impl std::cmp::Eq for EncodableSourceId {}
    606
    607#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone)]
    608pub struct EncodablePackageId {
    609    name: String,
    610    version: Option<String>,
    611    source: Option<EncodableSourceId>,
    612}
    613
    614impl fmt::Display for EncodablePackageId {
    615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    616        write!(f, "{}", self.name)?;
    617        if let Some(s) = &self.version {
    618            write!(f, " {}", s)?;
    619        }
    620        if let Some(s) = &self.source {
    621            write!(f, " ({})", s.as_url())?;
    622        }
    623        Ok(())
    624    }
    625}
    626
    627impl FromStr for EncodablePackageId {
    628    type Err = anyhow::Error;
    629
    630    fn from_str(s: &str) -> CargoResult<EncodablePackageId> {
    631        let mut s = s.splitn(3, ' ');
    632        let name = s.next().unwrap();
    633        let version = s.next();
    634        let source_id = match s.next() {
    635            Some(s) => {
    636                if let Some(s) = s.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
    637                    Some(SourceId::from_url(s)?)
    638                } else {
    639                    anyhow::bail!("invalid serialized PackageId")
    640                }
    641            }
    642            None => None,
    643        };
    644
    645        Ok(EncodablePackageId {
    646            name: name.to_string(),
    647            version: version.map(|v| v.to_string()),
    648            // Default to url encoded.
    649            source: source_id.map(EncodableSourceId::new),
    650        })
    651    }
    652}
    653
    654impl ser::Serialize for EncodablePackageId {
    655    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    656    where
    657        S: ser::Serializer,
    658    {
    659        s.collect_str(self)
    660    }
    661}
    662
    663impl<'de> de::Deserialize<'de> for EncodablePackageId {
    664    fn deserialize<D>(d: D) -> Result<EncodablePackageId, D::Error>
    665    where
    666        D: de::Deserializer<'de>,
    667    {
    668        String::deserialize(d).and_then(|string| {
    669            string
    670                .parse::<EncodablePackageId>()
    671                .map_err(de::Error::custom)
    672        })
    673    }
    674}
    675
    676impl ser::Serialize for Resolve {
    677    #[tracing::instrument(skip_all)]
    678    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    679    where
    680        S: ser::Serializer,
    681    {
    682        let mut ids: Vec<_> = self.iter().collect();
    683        ids.sort();
    684
    685        let state = EncodeState::new(self);
    686
    687        let encodable = ids
    688            .iter()
    689            .map(|&id| encodable_resolve_node(id, self, &state))
    690            .collect::<Vec<_>>();
    691
    692        let mut metadata = self.metadata().clone();
    693
    694        if self.version() == ResolveVersion::V1 {
    695            for &id in ids.iter().filter(|id| !id.source_id().is_path()) {
    696                let checksum = match self.checksums()[&id] {
    697                    Some(ref s) => &s[..],
    698                    None => "<none>",
    699                };
    700                let id = encodable_package_id(id, &state, self.version());
    701                metadata.insert(format!("checksum {}", id), checksum.to_string());
    702            }
    703        }
    704
    705        let metadata = if metadata.is_empty() {
    706            None
    707        } else {
    708            Some(metadata)
    709        };
    710
    711        let patch = Patch {
    712            unused: self
    713                .unused_patches()
    714                .iter()
    715                .map(|id| EncodableDependency {
    716                    name: id.name().to_string(),
    717                    version: id.version().to_string(),
    718                    source: encodable_source_id(id.source_id(), self.version()),
    719                    dependencies: None,
    720                    replace: None,
    721                    checksum: if self.version() >= ResolveVersion::V2 {
    722                        self.checksums().get(id).and_then(|x| x.clone())
    723                    } else {
    724                        None
    725                    },
    726                })
    727                .collect(),
    728        };
    729        EncodableResolve {
    730            package: Some(encodable),
    731            root: None,
    732            metadata,
    733            patch,
    734            version: match self.version() {
    735                ResolveVersion::V5 => Some(5),
    736                ResolveVersion::V4 => Some(4),
    737                ResolveVersion::V3 => Some(3),
    738                ResolveVersion::V2 | ResolveVersion::V1 => None,
    739            },
    740        }
    741        .serialize(s)
    742    }
    743}
    744
    745pub struct EncodeState<'a> {
    746    counts: Option<HashMap<InternedString, HashMap<&'a semver::Version, usize>>>,
    747}
    748
    749impl<'a> EncodeState<'a> {
    750    pub fn new(resolve: &'a Resolve) -> EncodeState<'a> {
    751        let counts = if resolve.version() >= ResolveVersion::V2 {
    752            let mut map = HashMap::new();
    753            for id in resolve.iter() {
    754                let slot = map
    755                    .entry(id.name())
    756                    .or_insert_with(HashMap::new)
    757                    .entry(id.version())
    758                    .or_insert(0);
    759                *slot += 1;
    760            }
    761            Some(map)
    762        } else {
    763            None
    764        };
    765        EncodeState { counts }
    766    }
    767}
    768
    769fn encodable_resolve_node(
    770    id: PackageId,
    771    resolve: &Resolve,
    772    state: &EncodeState<'_>,
    773) -> EncodableDependency {
    774    let (replace, deps) = match resolve.replacement(id) {
    775        Some(id) => (
    776            Some(encodable_package_id(id, state, resolve.version())),
    777            None,
    778        ),
    779        None => {
    780            let mut deps = resolve
    781                .deps_not_replaced(id)
    782                .map(|(id, _)| encodable_package_id(id, state, resolve.version()))
    783                .collect::<Vec<_>>();
    784            deps.sort();
    785            (None, Some(deps))
    786        }
    787    };
    788
    789    EncodableDependency {
    790        name: id.name().to_string(),
    791        version: id.version().to_string(),
    792        source: encodable_source_id(id.source_id(), resolve.version()),
    793        dependencies: deps,
    794        replace,
    795        checksum: if resolve.version() >= ResolveVersion::V2 {
    796            resolve.checksums().get(&id).and_then(|s| s.clone())
    797        } else {
    798            None
    799        },
    800    }
    801}
    802
    803pub fn encodable_package_id(
    804    id: PackageId,
    805    state: &EncodeState<'_>,
    806    resolve_version: ResolveVersion,
    807) -> EncodablePackageId {
    808    let mut version = Some(id.version().to_string());
    809    let mut id_to_encode = id.source_id();
    810    if resolve_version <= ResolveVersion::V2 {
    811        if let Some(GitReference::Branch(b)) = id_to_encode.git_reference() {
    812            if b == "master" {
    813                id_to_encode =
    814                    SourceId::for_git(id_to_encode.url(), GitReference::DefaultBranch).unwrap();
    815            }
    816        }
    817    }
    818    let mut source = encodable_source_id(id_to_encode.without_precise(), resolve_version);
    819    if let Some(counts) = &state.counts {
    820        let version_counts = &counts[&id.name()];
    821        if version_counts[&id.version()] == 1 {
    822            source = None;
    823            if version_counts.len() == 1 {
    824                version = None;
    825            }
    826        }
    827    }
    828    EncodablePackageId {
    829        name: id.name().to_string(),
    830        version,
    831        source,
    832    }
    833}
    834
    835fn encodable_source_id(id: SourceId, version: ResolveVersion) -> Option<EncodableSourceId> {
    836    if id.is_path() {
    837        None
    838    } else {
    839        Some(if version >= ResolveVersion::V4 {
    840            EncodableSourceId::new(id)
    841        } else {
    842            EncodableSourceId::without_url_encoded(id)
    843        })
    844    }
    845}