cargo_plumbing/cargo/core/resolver/
encode.rs1use 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#[derive(Serialize, Deserialize, Debug)]
25pub struct EncodableResolve {
26 pub version: Option<u32>,
27 package: Option<Vec<EncodableDependency>>,
28 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 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 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}