cargo_plumbing/cargo/core/resolver/
encode.rs

1//! Definition of how to encode a `Resolve` into a TOML `Cargo.lock` file
2//!
3//! This module is a temporary copy from the cargo codebase.
4
5use std::collections::{BTreeMap, HashMap, HashSet};
6use std::fmt;
7use std::str::FromStr;
8
9use anyhow::Context;
10use cargo::core::{Dependency, Package, Workspace};
11use cargo::util::internal;
12use cargo::util::interning::InternedString;
13use cargo::{
14    core::{GitReference, PackageId, PackageIdSpec, Resolve, ResolveVersion, SourceId, SourceKind},
15    CargoResult,
16};
17use cargo_plumbing_schemas::lockfile::{
18    NormalizedDependency, NormalizedPatch, NormalizedResolve, Precise,
19};
20use serde::{de, ser, Deserialize, Serialize};
21use url::Url;
22
23/// The `Cargo.lock` structure.
24#[derive(Serialize, Deserialize, Debug)]
25pub struct EncodableResolve {
26    pub version: Option<u32>,
27    package: Option<Vec<EncodableDependency>>,
28    /// `root` is optional to allow backward compatibility.
29    root: Option<EncodableDependency>,
30    metadata: Option<Metadata>,
31    #[serde(default, skip_serializing_if = "Patch::is_empty")]
32    patch: Patch,
33}
34
35pub fn build_path_deps(
36    ws: &Workspace<'_>,
37) -> CargoResult<HashMap<String, HashMap<semver::Version, SourceId>>> {
38    // If a crate is **not** a path source, then we're probably in a situation
39    // such as `cargo install` with a lock file from a remote dependency. In
40    // that case we don't need to fixup any path dependencies (as they're not
41    // actually path dependencies any more), so we ignore them.
42    let members = ws
43        .members()
44        .filter(|p| p.package_id().source_id().is_path())
45        .collect::<Vec<_>>();
46
47    let mut ret: HashMap<String, HashMap<semver::Version, SourceId>> = HashMap::new();
48    let mut visited = HashSet::new();
49    for member in members.iter() {
50        ret.entry(member.package_id().name().to_string())
51            .or_default()
52            .insert(
53                member.package_id().version().clone(),
54                member.package_id().source_id(),
55            );
56        visited.insert(member.package_id().source_id());
57    }
58    for member in members.iter() {
59        build_pkg(member, ws, &mut ret, &mut visited);
60    }
61    for deps in ws.root_patch()?.values() {
62        for dep in deps {
63            build_dep(dep, ws, &mut ret, &mut visited);
64        }
65    }
66    for (_, dep) in ws.root_replace() {
67        build_dep(dep, ws, &mut ret, &mut visited);
68    }
69
70    return Ok(ret);
71
72    fn build_pkg(
73        pkg: &Package,
74        ws: &Workspace<'_>,
75        ret: &mut HashMap<String, HashMap<semver::Version, SourceId>>,
76        visited: &mut HashSet<SourceId>,
77    ) {
78        for dep in pkg.dependencies() {
79            build_dep(dep, ws, ret, visited);
80        }
81    }
82
83    fn build_dep(
84        dep: &Dependency,
85        ws: &Workspace<'_>,
86        ret: &mut HashMap<String, HashMap<semver::Version, SourceId>>,
87        visited: &mut HashSet<SourceId>,
88    ) {
89        let id = dep.source_id();
90        if visited.contains(&id) || !id.is_path() {
91            return;
92        }
93        let path = match id.url().to_file_path() {
94            Ok(p) => p.join("Cargo.toml"),
95            Err(_) => return,
96        };
97        let Ok(pkg) = ws.load(&path) else { return };
98        ret.entry(pkg.package_id().name().to_string())
99            .or_default()
100            .insert(
101                pkg.package_id().version().clone(),
102                pkg.package_id().source_id(),
103            );
104        visited.insert(pkg.package_id().source_id());
105        build_pkg(&pkg, ws, ret, visited);
106    }
107}
108
109#[derive(Serialize, Deserialize, Debug, Default)]
110struct Patch {
111    unused: Vec<EncodableDependency>,
112}
113
114impl EncodableResolve {
115    pub fn normalize(self) -> CargoResult<NormalizedResolve> {
116        let package = normalize_packages(self.root, self.package, self.metadata)?;
117
118        Ok(NormalizedResolve {
119            package,
120            patch: self.patch.normalize()?,
121        })
122    }
123}
124
125pub fn normalize_packages(
126    root: Option<EncodableDependency>,
127    packages: Option<Vec<EncodableDependency>>,
128    metadata: Option<Metadata>,
129) -> CargoResult<Vec<NormalizedDependency>> {
130    let mut metadata_map = {
131        let mut metadata_map = HashMap::new();
132        if let Some(metadata) = metadata {
133            let prefix = "checksum ";
134            for (k, v) in metadata {
135                let k = k.strip_prefix(prefix).unwrap();
136                let id = k
137                    .parse::<EncodablePackageId>()
138                    .with_context(|| internal("invalid encoding of checksum in lockfile"))?
139                    .normalize()?;
140                metadata_map.insert(id, v);
141            }
142        }
143        metadata_map
144    };
145
146    let package = {
147        let mut normalized_packages = Vec::new();
148        if let Some(pkgs) = packages {
149            for pkg in pkgs {
150                let mut pkg = pkg.normalize()?;
151                if pkg.checksum.is_none() {
152                    pkg.checksum = metadata_map.remove(&pkg.id);
153                }
154                normalized_packages.push(pkg);
155            }
156        }
157        if let Some(pkg) = root {
158            let mut pkg = pkg.normalize()?;
159            if pkg.checksum.is_none() {
160                pkg.checksum = metadata_map.remove(&pkg.id);
161            }
162            normalized_packages.push(pkg);
163        }
164        normalized_packages
165    };
166
167    Ok(package)
168}
169
170pub type Metadata = BTreeMap<String, String>;
171
172impl Patch {
173    pub(crate) fn normalize(self) -> CargoResult<NormalizedPatch> {
174        let unused = self
175            .unused
176            .into_iter()
177            .map(|u| u.normalize())
178            .collect::<Result<Vec<_>, _>>()?;
179        Ok(NormalizedPatch { unused })
180    }
181
182    fn is_empty(&self) -> bool {
183        self.unused.is_empty()
184    }
185}
186
187#[derive(Serialize, Deserialize, Debug)]
188pub struct EncodableDependency {
189    pub name: String,
190    pub version: String,
191    pub source: Option<EncodableSourceId>,
192    pub checksum: Option<String>,
193    pub dependencies: Option<Vec<EncodablePackageId>>,
194    pub replace: Option<EncodablePackageId>,
195}
196
197impl EncodableDependency {
198    pub fn normalize(self) -> CargoResult<NormalizedDependency> {
199        let mut id = PackageIdSpec::new(self.name).with_version(self.version.parse()?);
200        let mut source = None;
201
202        if let Some(s) = self.source {
203            id = id.with_url(s.url.clone()).with_kind(s.kind.clone());
204            source = Some(s);
205        }
206
207        let dependencies = match self.dependencies {
208            Some(deps) => Some(
209                deps.into_iter()
210                    .map(|d| d.normalize())
211                    .collect::<Result<Vec<_>, _>>()?,
212            ),
213            None => None,
214        };
215
216        let replace = match self.replace {
217            Some(replace) => Some(replace.normalize()?),
218            None => None,
219        };
220
221        let rev = match source {
222            Some(s) if matches!(s.kind, SourceKind::Git(..)) => s.precise,
223            _ => None,
224        };
225
226        Ok(NormalizedDependency {
227            id,
228            rev,
229            checksum: self.checksum,
230            dependencies,
231            replace,
232        })
233    }
234}
235
236#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
237pub struct EncodableSourceId {
238    pub kind: SourceKind,
239    pub url: Url,
240    pub precise: Option<Precise>,
241    pub encoded: bool,
242}
243
244impl EncodableSourceId {
245    pub fn new(url: Url, precise: Option<&'static str>, kind: SourceKind) -> Self {
246        Self {
247            url,
248            kind,
249            encoded: true,
250            precise: precise.map(|s| {
251                if s == "locked" {
252                    Precise::Locked
253                } else {
254                    Precise::GitUrlFragment(s.to_owned())
255                }
256            }),
257        }
258    }
259
260    pub fn without_url_encoded(url: Url, precise: Option<&'static str>, kind: SourceKind) -> Self {
261        Self {
262            url,
263            kind,
264            encoded: false,
265            precise: precise.map(|s| {
266                if s == "locked" {
267                    Precise::Locked
268                } else {
269                    Precise::GitUrlFragment(s.to_owned())
270                }
271            }),
272        }
273    }
274
275    pub fn from_url(string: &str) -> CargoResult<Self> {
276        let (kind, url) = string
277            .split_once('+')
278            .ok_or_else(|| anyhow::format_err!("invalid source `{}`", string))?;
279
280        match kind {
281            "git" => {
282                let mut url = str_to_url(url)?;
283                let reference = GitReference::from_query(url.query_pairs());
284                let precise = url.fragment().map(|s| s.to_owned());
285                url.set_fragment(None);
286                url.set_query(None);
287                Ok(Self {
288                    url,
289                    kind: SourceKind::Git(reference),
290                    encoded: false,
291                    precise: precise.map(Precise::GitUrlFragment),
292                })
293            }
294            "registry" => {
295                let url = str_to_url(url)?;
296                Ok(Self {
297                    url,
298                    kind: SourceKind::Registry,
299                    encoded: false,
300                    precise: Some(Precise::Locked),
301                })
302            }
303            "sparse" => {
304                let url = str_to_url(string)?;
305                Ok(Self {
306                    url,
307                    kind: SourceKind::SparseRegistry,
308                    encoded: false,
309                    precise: Some(Precise::Locked),
310                })
311            }
312            "path" => {
313                let url = str_to_url(url)?;
314                Ok(Self {
315                    url,
316                    kind: SourceKind::Path,
317                    encoded: false,
318                    precise: None,
319                })
320            }
321            kind => Err(anyhow::format_err!("unsupported source protocol: {}", kind)),
322        }
323    }
324
325    fn is_path(&self) -> bool {
326        self.kind == SourceKind::Path
327    }
328}
329
330impl fmt::Display for EncodableSourceId {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        if let Some(protocol) = self.kind.protocol() {
333            write!(f, "{protocol}+")?;
334        }
335        write!(f, "{}", self.url)?;
336        if let Self {
337            kind: SourceKind::Git(ref reference),
338            ref precise,
339            ..
340        } = self
341        {
342            if let Some(pretty) = reference.pretty_ref(true) {
343                write!(f, "?{pretty}")?;
344            }
345            if let Some(precise) = precise.as_ref() {
346                write!(f, "#{precise}")?;
347            }
348        }
349        Ok(())
350    }
351}
352
353impl Serialize for EncodableSourceId {
354    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
355    where
356        S: ser::Serializer,
357    {
358        if self.is_path() {
359            None::<String>.serialize(s)
360        } else {
361            s.collect_str(self)
362        }
363    }
364}
365
366impl<'de> Deserialize<'de> for EncodableSourceId {
367    fn deserialize<D>(d: D) -> Result<Self, D::Error>
368    where
369        D: de::Deserializer<'de>,
370    {
371        let string = String::deserialize(d)?;
372        Self::from_url(&string).map_err(de::Error::custom)
373    }
374}
375
376#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
377pub struct EncodablePackageId {
378    name: String,
379    version: Option<String>,
380    source: Option<EncodableSourceId>,
381}
382
383impl EncodablePackageId {
384    pub fn normalize(self) -> CargoResult<PackageIdSpec> {
385        let mut id = PackageIdSpec::new(self.name);
386
387        if let Some(version) = self.version {
388            id = id.with_version(version.parse()?);
389        }
390
391        if let Some(source) = self.source {
392            id = id.with_url(source.url).with_kind(source.kind);
393        }
394
395        Ok(id)
396    }
397}
398
399impl fmt::Display for EncodablePackageId {
400    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        write!(f, "{}", self.name)?;
402        if let Some(s) = &self.version {
403            write!(f, " {s}")?;
404        }
405        if let Some(s) = &self.source {
406            write!(f, " ({s})")?;
407        }
408        Ok(())
409    }
410}
411
412impl FromStr for EncodablePackageId {
413    type Err = anyhow::Error;
414
415    fn from_str(s: &str) -> CargoResult<EncodablePackageId> {
416        let mut s = s.splitn(3, ' ');
417        let name = s.next().unwrap();
418        let version = s.next();
419        let source_id = match s.next() {
420            Some(s) => {
421                if let Some(s) = s.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
422                    Some(EncodableSourceId::from_url(s)?)
423                } else {
424                    anyhow::bail!("invalid serialized PackageId")
425                }
426            }
427            None => None,
428        };
429
430        Ok(EncodablePackageId {
431            name: name.to_owned(),
432            version: version.map(|v| v.to_owned()),
433            // Default to url encoded.
434            source: source_id,
435        })
436    }
437}
438
439impl Serialize for EncodablePackageId {
440    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
441    where
442        S: ser::Serializer,
443    {
444        s.collect_str(self)
445    }
446}
447
448impl<'de> Deserialize<'de> for EncodablePackageId {
449    fn deserialize<D>(d: D) -> Result<EncodablePackageId, D::Error>
450    where
451        D: de::Deserializer<'de>,
452    {
453        String::deserialize(d).and_then(|string| {
454            string
455                .parse::<EncodablePackageId>()
456                .map_err(de::Error::custom)
457        })
458    }
459}
460
461fn str_to_url(string: &str) -> CargoResult<Url> {
462    Url::parse(string).map_err(|s| {
463        if string.starts_with("git@") {
464            anyhow::format_err!(
465                "invalid url `{}`: {}; try using `{}` instead",
466                string,
467                s,
468                format_args!("ssh://{}", string.replacen(':', "/", 1))
469            )
470        } else {
471            anyhow::format_err!("invalid url `{}`: {}", string, s)
472        }
473    })
474}
475
476pub struct EncodeState<'a> {
477    counts: Option<HashMap<InternedString, HashMap<&'a semver::Version, usize>>>,
478}
479
480impl<'a> EncodeState<'a> {
481    pub fn new(resolve: &'a Resolve) -> EncodeState<'a> {
482        let counts = if resolve.version() >= ResolveVersion::V2 {
483            let mut map = HashMap::new();
484            for id in resolve.iter() {
485                let slot = map
486                    .entry(id.name())
487                    .or_insert_with(HashMap::new)
488                    .entry(id.version())
489                    .or_insert(0);
490                *slot += 1;
491            }
492            Some(map)
493        } else {
494            None
495        };
496        EncodeState { counts }
497    }
498}
499
500pub fn encodable_resolve_node(
501    id: PackageId,
502    resolve: &Resolve,
503    state: &EncodeState<'_>,
504) -> EncodableDependency {
505    let (replace, deps) = match resolve.replacement(id) {
506        Some(id) => (
507            Some(encodable_package_id(id, state, resolve.version())),
508            None,
509        ),
510        None => {
511            let mut deps = resolve
512                .deps_not_replaced(id)
513                .map(|(id, _)| encodable_package_id(id, state, resolve.version()))
514                .collect::<Vec<_>>();
515            deps.sort();
516            (None, if deps.is_empty() { None } else { Some(deps) })
517        }
518    };
519
520    EncodableDependency {
521        name: id.name().to_string(),
522        version: id.version().to_string(),
523        source: encodable_source_id(id.source_id(), resolve.version()),
524        dependencies: deps,
525        replace,
526        checksum: if resolve.version() >= ResolveVersion::V2 {
527            resolve.checksums().get(&id).and_then(|s| s.clone())
528        } else {
529            None
530        },
531    }
532}
533
534pub fn encodable_package_id(
535    id: PackageId,
536    state: &EncodeState<'_>,
537    resolve_version: ResolveVersion,
538) -> EncodablePackageId {
539    let mut version = Some(id.version().to_string());
540    let mut id_to_encode = id.source_id();
541    if resolve_version <= ResolveVersion::V2 {
542        if let Some(GitReference::Branch(b)) = id_to_encode.git_reference() {
543            if b == "master" {
544                id_to_encode =
545                    SourceId::for_git(id_to_encode.url(), GitReference::DefaultBranch).unwrap();
546            }
547        }
548    }
549    let mut source = encodable_source_id(id_to_encode.without_precise(), resolve_version);
550    if let Some(counts) = &state.counts {
551        let version_counts = &counts[&id.name()];
552        if version_counts[&id.version()] == 1 {
553            source = None;
554            if version_counts.len() == 1 {
555                version = None;
556            }
557        }
558    }
559    EncodablePackageId {
560        name: id.name().to_string(),
561        version,
562        source,
563    }
564}
565
566pub fn encodable_source_id(id: SourceId, version: ResolveVersion) -> Option<EncodableSourceId> {
567    if id.is_path() {
568        None
569    } else {
570        Some(if version >= ResolveVersion::V4 {
571            EncodableSourceId::new(
572                id.url().clone(),
573                id.precise_git_fragment(),
574                id.kind().clone(),
575            )
576        } else {
577            EncodableSourceId::without_url_encoded(
578                id.url().clone(),
579                id.precise_git_fragment(),
580                id.kind().clone(),
581            )
582        })
583    }
584}