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