cargo_lock/lockfile/
encoding.rs

1//! serde-based `Cargo.lock` parser/serializer
2//!
3//! Customized to allow pre/postprocessing to detect and serialize both
4//! the V1 vs V2 formats and ensure the end-user is supplied a consistent
5//! representation regardless of which version is in use.
6//!
7//! Parts adapted from upstream Cargo.
8//! Cargo is primarily distributed under the terms of both the MIT license and
9//! the Apache License (Version 2.0).
10
11use super::{Lockfile, ResolveVersion};
12use crate::{
13    metadata, Checksum, Dependency, Error, Metadata, Name, Package, Patch, Result, SourceId,
14    Version,
15};
16use serde::{de, ser, Deserialize, Serialize};
17use std::{collections::HashMap, collections::HashSet, fmt, fmt::Write, str::FromStr};
18
19impl<'de> Deserialize<'de> for Lockfile {
20    fn deserialize<D: de::Deserializer<'de>>(
21        deserializer: D,
22    ) -> std::result::Result<Self, D::Error> {
23        EncodableLockfile::deserialize(deserializer)?
24            .try_into()
25            .map_err(de::Error::custom)
26    }
27}
28
29impl Serialize for Lockfile {
30    fn serialize<S: ser::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
31        EncodableLockfile::from(self).serialize(serializer)
32    }
33}
34
35/// Serialization-oriented equivalent to [`Lockfile`]
36#[derive(Debug, Deserialize, Serialize)]
37pub(super) struct EncodableLockfile {
38    /// Lockfile version
39    pub(super) version: Option<u32>,
40
41    /// Packages in the lockfile
42    #[serde(default)]
43    pub(super) package: Vec<EncodablePackage>,
44
45    /// Legacy root package (preserved for compatibility)
46    pub(super) root: Option<EncodablePackage>,
47
48    /// Metadata fields
49    #[serde(default, skip_serializing_if = "Metadata::is_empty")]
50    pub(super) metadata: Metadata,
51
52    /// Patch section
53    #[serde(default, skip_serializing_if = "Patch::is_empty")]
54    pub(super) patch: Patch,
55}
56
57impl EncodableLockfile {
58    /// Attempt to find a checksum for a package in a V1 lockfile
59    pub fn find_checksum(&self, package: &Package) -> Option<Checksum> {
60        for (key, value) in &self.metadata {
61            if let Ok(dep) = key.checksum_dependency() {
62                if dep.name == package.name && dep.version == package.version {
63                    return value.checksum().ok();
64                }
65            }
66        }
67
68        None
69    }
70}
71
72impl TryFrom<EncodableLockfile> for Lockfile {
73    type Error = Error;
74
75    fn try_from(raw_lockfile: EncodableLockfile) -> Result<Lockfile> {
76        let version = match raw_lockfile.version {
77            Some(n) => n.try_into()?,
78            None => ResolveVersion::detect(&raw_lockfile.package, &raw_lockfile.metadata)?,
79        };
80
81        let mut packages = Vec::with_capacity(raw_lockfile.package.len());
82
83        for raw_package in &raw_lockfile.package {
84            packages.push(if version == ResolveVersion::V1 {
85                // In the V1 format, all dependencies are fully qualified with
86                // their versions, but their checksums are stored in metadata.
87                let mut pkg = Package::try_from(raw_package)?;
88                pkg.checksum = raw_lockfile.find_checksum(&pkg);
89                pkg
90            } else {
91                // In newer versions, we may need to look up dependency versions
92                // from the other packages listed in the lockfile
93                raw_package.resolve(&raw_lockfile.package)?
94            })
95        }
96
97        Ok(Lockfile {
98            version,
99            packages,
100            root: raw_lockfile
101                .root
102                .as_ref()
103                .map(|root| root.try_into())
104                .transpose()?,
105            metadata: raw_lockfile.metadata,
106            patch: raw_lockfile.patch,
107        })
108    }
109}
110
111impl From<&Lockfile> for EncodableLockfile {
112    fn from(lockfile: &Lockfile) -> EncodableLockfile {
113        let mut packages = Vec::with_capacity(lockfile.packages.len());
114        let mut metadata = lockfile.metadata.clone();
115
116        let mut package_to_registries: HashMap<_, HashSet<&SourceId>> = HashMap::new();
117        for package in &lockfile.packages {
118            if let Some(source) = package.source.as_ref() {
119                if source.is_registry() {
120                    package_to_registries
121                        .entry(&package.name)
122                        .or_default()
123                        .insert(source);
124                }
125            }
126        }
127
128        for package in &lockfile.packages {
129            let mut raw_pkg = EncodablePackage::from_package(package, lockfile.version);
130            let checksum_key = metadata::MetadataKey::for_checksum(&Dependency::from(package));
131
132            if lockfile.version == ResolveVersion::V1 {
133                // In the V1 format, we need to remove the checksum from
134                // packages and add it to metadata
135                if let Some(checksum) = raw_pkg.checksum.take() {
136                    let value = checksum
137                        .to_string()
138                        .parse::<metadata::MetadataValue>()
139                        .unwrap();
140                    metadata.insert(checksum_key, value);
141                }
142            } else {
143                // In newer versions, we need to remove the version/source from
144                // unambiguous dependencies, and remove checksums from the
145                // metadata table if present
146                raw_pkg.v2_deps(&lockfile.packages);
147                metadata.remove(&checksum_key);
148
149                // If there's only one registry, Cargo does not encode this information so
150                // it should be omitted from encodable packages
151                for dep in raw_pkg.dependencies.iter_mut() {
152                    if let Some(registries) = package_to_registries.get(&dep.name) {
153                        if registries.len() == 1 {
154                            dep.source = None;
155                        }
156                    }
157                }
158            }
159
160            packages.push(raw_pkg);
161        }
162
163        let version = if lockfile.version.is_explicit() {
164            Some(lockfile.version.into())
165        } else {
166            None
167        };
168
169        EncodableLockfile {
170            version,
171            package: packages,
172            root: lockfile
173                .root
174                .as_ref()
175                .map(|root| EncodablePackage::from_package(root, lockfile.version)),
176            metadata,
177            patch: lockfile.patch.clone(),
178        }
179    }
180}
181
182#[allow(clippy::to_string_trait_impl)]
183impl ToString for EncodableLockfile {
184    /// Adapted from `serialize_resolve` in upstream Cargo:
185    /// <https://github.com/rust-lang/cargo/blob/0c70319/src/cargo/ops/lockfile.rs#L103-L174>
186    fn to_string(&self) -> String {
187        let toml = toml::Value::try_from(self).unwrap();
188        let mut out = String::new();
189
190        // At the start of the file we notify the reader that the file is generated.
191        // Specifically Phabricator ignores files containing "@generated", so we use that.
192        let marker_line = "# This file is automatically @generated by Cargo.";
193        let extra_line = "# It is not intended for manual editing.";
194        out.push_str(marker_line);
195        out.push('\n');
196        out.push_str(extra_line);
197        out.push('\n');
198
199        if let Some(value) = toml.get("version") {
200            if let Some(version) = value.as_integer() {
201                if version >= 3 {
202                    writeln!(out, "version = {version}").unwrap();
203                }
204            }
205        }
206
207        out.push('\n');
208
209        let deps = toml["package"].as_array().unwrap();
210        for dep in deps {
211            let dep = dep.as_table().unwrap();
212
213            out.push_str("[[package]]\n");
214            emit_package(dep, &mut out);
215        }
216
217        if let Some(patch) = toml.get("patch") {
218            let list = patch["unused"].as_array().unwrap();
219            for entry in list {
220                out.push_str("[[patch.unused]]\n");
221                emit_package(entry.as_table().unwrap(), &mut out);
222                out.push('\n');
223            }
224        }
225
226        if let Some(meta) = toml.get("metadata") {
227            out.push_str("[metadata]\n");
228            out.push_str(&toml::to_string_pretty(&meta).unwrap());
229        }
230
231        // Trim redundant newlines
232        if out.ends_with("\n\n") {
233            out.pop();
234        }
235
236        out
237    }
238}
239
240/// Emit a single package from a lockfile.
241///
242/// This method is adapted from the same-named method in upstream Cargo:
243/// <https://github.com/rust-lang/cargo/blob/0c70319/src/cargo/ops/lockfile.rs#L194-L221>
244fn emit_package(dep: &toml::value::Table, out: &mut String) {
245    writeln!(out, "name = {}", &dep["name"]).unwrap();
246    writeln!(out, "version = {}", &dep["version"]).unwrap();
247
248    if dep.contains_key("source") {
249        writeln!(out, "source = {}", &dep["source"]).unwrap();
250    }
251    if dep.contains_key("checksum") {
252        writeln!(out, "checksum = {}", &dep["checksum"]).unwrap();
253    }
254
255    if let Some(s) = dep.get("dependencies") {
256        let slice = s.as_array().unwrap();
257
258        if !slice.is_empty() {
259            out.push_str("dependencies = [\n");
260
261            for child in slice.iter() {
262                writeln!(out, " {child},").unwrap();
263            }
264
265            out.push_str("]\n");
266        }
267    } else if dep.contains_key("replace") {
268        writeln!(out, "replace = {}", &dep["replace"]).unwrap();
269    }
270
271    out.push('\n');
272}
273
274/// Serialization-oriented equivalent to [`Package`]
275#[derive(Debug, Deserialize, Serialize)]
276pub(crate) struct EncodablePackage {
277    /// Package name
278    pub(super) name: Name,
279
280    /// Package version
281    pub(super) version: Version,
282
283    /// Source of a package
284    pub(super) source: Option<EncodableSourceId>,
285
286    /// Package checksum
287    pub(super) checksum: Option<Checksum>,
288
289    /// Package dependencies
290    #[serde(default, skip_serializing_if = "Vec::is_empty")]
291    pub(super) dependencies: Vec<EncodableDependency>,
292
293    /// Replace directive
294    pub(super) replace: Option<EncodableDependency>,
295}
296
297impl EncodablePackage {
298    /// Resolve all of the dependencies of a package, which in the V2 format
299    /// may be abbreviated to prevent merge conflicts
300    fn resolve(&self, packages: &[EncodablePackage]) -> Result<Package> {
301        let mut dependencies = Vec::with_capacity(self.dependencies.len());
302
303        for dep in &self.dependencies {
304            dependencies.push(dep.resolve(packages)?);
305        }
306
307        Ok(Package {
308            name: self.name.clone(),
309            version: self.version.clone(),
310            source: self.source.as_ref().map(|s| s.inner.clone()),
311            checksum: self.checksum.clone(),
312            dependencies,
313            replace: self
314                .replace
315                .as_ref()
316                .map(|rep| rep.try_into())
317                .transpose()?,
318        })
319    }
320
321    /// Prepare `ResolveVersion::V2` dependencies by removing ones which are unambiguous
322    fn v2_deps(&mut self, packages: &[Package]) {
323        for dependency in &mut self.dependencies {
324            dependency.v2(packages);
325        }
326    }
327
328    fn from_package(package: &Package, version: ResolveVersion) -> EncodablePackage {
329        EncodablePackage {
330            name: package.name.clone(),
331            version: package.version.clone(),
332            source: package
333                .source
334                .clone()
335                .and_then(|id| encodable_source_id(id, version)),
336            checksum: package.checksum.clone(),
337            dependencies: package
338                .dependencies
339                .iter()
340                .map(|dep| EncodableDependency::from_dependency(dep, version))
341                .collect::<Vec<_>>(),
342            replace: package
343                .replace
344                .as_ref()
345                .map(|rep| EncodableDependency::from_dependency(rep, version)),
346        }
347    }
348}
349
350fn encodable_source_id(id: SourceId, version: ResolveVersion) -> Option<EncodableSourceId> {
351    if id.is_path() {
352        None
353    } else {
354        Some(if version >= ResolveVersion::V4 {
355            EncodableSourceId::new(id)
356        } else {
357            EncodableSourceId::without_url_encoded(id)
358        })
359    }
360}
361
362/// Note: this only works for `ResolveVersion::V1` dependencies.
363impl TryFrom<&EncodablePackage> for Package {
364    type Error = Error;
365
366    fn try_from(raw_package: &EncodablePackage) -> Result<Package> {
367        raw_package.resolve(&[])
368    }
369}
370
371/// Package dependencies
372#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
373pub(crate) struct EncodableDependency {
374    /// Name of the dependency
375    pub(super) name: Name,
376
377    /// Version of the dependency
378    pub(super) version: Option<Version>,
379
380    /// Source for the dependency
381    pub(super) source: Option<EncodableSourceId>,
382}
383
384impl EncodableDependency {
385    /// Resolve this dependency, which in the V2 format may be abbreviated to
386    /// prevent merge conflicts
387    pub fn resolve(&self, packages: &[EncodablePackage]) -> Result<Dependency> {
388        for pkg in packages {
389            if pkg.name == self.name
390                && (self.version.is_none() || self.version.as_ref() == Some(&pkg.version))
391                && self.source.is_none()
392            {
393                return Ok(Dependency {
394                    name: pkg.name.clone(),
395                    version: pkg.version.clone(),
396                    source: pkg
397                        .source
398                        .clone()
399                        .map(|x| x.normalize_git_source_for_dependency()),
400                });
401            }
402        }
403
404        let version = self
405            .version
406            .clone()
407            .ok_or_else(|| Error::Parse(format!("couldn't resolve dependency: {}", self.name)))?;
408
409        Ok(Dependency {
410            name: self.name.clone(),
411            version,
412            source: self
413                .source
414                .clone()
415                .map(|x| x.normalize_git_source_for_dependency()),
416        })
417    }
418
419    /// Prepare `ResolveVersion::V2` dependencies by removing ones which are unambiguous
420    pub fn v2(&mut self, packages: &[Package]) {
421        let mut matching = vec![];
422
423        for package in packages {
424            if package.name == self.name {
425                matching.push(package);
426            }
427        }
428
429        // TODO(tarcieri): better handle other cases?
430        if matching.len() == 1 {
431            self.version = None;
432            self.source = None;
433        }
434    }
435
436    pub fn from_dependency(dep: &Dependency, version: ResolveVersion) -> EncodableDependency {
437        EncodableDependency {
438            name: dep.name.clone(),
439            version: Some(dep.version.clone()),
440            source: dep
441                .source
442                .clone()
443                .and_then(|id| encodable_source_id(id, version)),
444        }
445    }
446}
447
448/// Note: this only works for `ResolveVersion::V1` dependencies.
449impl FromStr for EncodableDependency {
450    type Err = Error;
451
452    fn from_str(s: &str) -> Result<Self> {
453        let mut parts = s.split_whitespace();
454
455        let name = parts
456            .next()
457            .ok_or_else(|| Error::Parse("empty dependency string".to_owned()))?
458            .parse()?;
459
460        let version = parts.next().map(FromStr::from_str).transpose()?;
461
462        let source = parts
463            .next()
464            .map(|s| {
465                if s.len() < 2 || !s.starts_with('(') || !s.ends_with(')') {
466                    Err(Error::Parse(format!("malformed source in dependency: {s}")))
467                } else {
468                    s[1..(s.len() - 1)].parse::<SourceId>()
469                }
470            })
471            .transpose()?;
472
473        if parts.next().is_some() {
474            return Err(Error::Parse(format!("malformed dependency: {s}")));
475        }
476
477        Ok(Self {
478            name,
479            version,
480            // `EncodableDependency::from_str` is found only used by MetadataKey,
481            // which is only a thing in lockfile v1.
482            // Hence, no need for url encoding.
483            source: source.map(EncodableSourceId::without_url_encoded),
484        })
485    }
486}
487
488impl fmt::Display for EncodableDependency {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        write!(f, "{}", &self.name)?;
491
492        if let Some(version) = &self.version {
493            write!(f, " {version}")?;
494        }
495
496        if let Some(source) = &self.source {
497            write!(f, " ({})", source.as_url())?;
498        }
499
500        Ok(())
501    }
502}
503
504/// Note: this only works for `ResolveVersion::V1` dependencies.
505impl TryFrom<&EncodableDependency> for Dependency {
506    type Error = Error;
507
508    fn try_from(raw_dependency: &EncodableDependency) -> Result<Dependency> {
509        raw_dependency.resolve(&[])
510    }
511}
512
513impl<'de> Deserialize<'de> for EncodableDependency {
514    fn deserialize<D: de::Deserializer<'de>>(
515        deserializer: D,
516    ) -> std::result::Result<Self, D::Error> {
517        String::deserialize(deserializer)?
518            .parse()
519            .map_err(de::Error::custom)
520    }
521}
522
523impl Serialize for EncodableDependency {
524    fn serialize<S: ser::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
525        self.to_string().serialize(serializer)
526    }
527}
528
529/// Pretty much equivalent to [`SourceId`] with a different serialization method.
530///
531/// The serialization for `SourceId` doesn't do URL encode for parameters.
532/// In contrast, this type is aware of that whenever [`ResolveVersion`] allows
533/// us to do so (v4 or later).
534#[derive(Deserialize, Debug, PartialOrd, Ord, Clone)]
535#[serde(transparent)]
536pub(super) struct EncodableSourceId {
537    inner: SourceId,
538    /// We don't care about the deserialization of this, as the `url` crate
539    /// will always decode as the URL was encoded.
540    #[serde(skip)]
541    encoded: bool,
542}
543
544impl EncodableSourceId {
545    /// Creates a `EncodableSourceId` that always encodes URL params.
546    fn new(inner: SourceId) -> Self {
547        Self {
548            inner,
549            encoded: true,
550        }
551    }
552
553    /// Creates a `EncodableSourceId` that doesn't encode URL params. This is
554    /// for backward compatibility for order lockfile version.
555    fn without_url_encoded(inner: SourceId) -> Self {
556        Self {
557            inner,
558            encoded: false,
559        }
560    }
561
562    /// Encodes the inner [`SourceId`] as a URL.
563    fn as_url(&self) -> impl fmt::Display + '_ {
564        self.inner.as_url(self.encoded)
565    }
566}
567
568impl std::ops::Deref for EncodableSourceId {
569    type Target = SourceId;
570
571    fn deref(&self) -> &Self::Target {
572        &self.inner
573    }
574}
575
576impl Serialize for EncodableSourceId {
577    fn serialize<S: ser::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
578        serializer.collect_str(&self.as_url())
579    }
580}
581
582impl std::hash::Hash for EncodableSourceId {
583    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
584        self.inner.hash(state)
585    }
586}
587
588impl PartialEq for EncodableSourceId {
589    fn eq(&self, other: &Self) -> bool {
590        self.inner == other.inner
591    }
592}
593
594impl Eq for EncodableSourceId {}