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 serialiation 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::{internal, Graph};
119use anyhow::{bail, Context as _};
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.to_string()), 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}