#
cargo_util_schemas/
lockfile.rs

    1//! `Cargo.lock` / Lockfile schema definition
    2
    3use std::collections::BTreeMap;
    4use std::fmt;
    5use std::{cmp::Ordering, str::FromStr};
    6
    7use serde::{Deserialize, Serialize, de, ser};
    8use url::Url;
    9
    10use crate::core::{GitReference, SourceKind};
    11
    12/// Serialization of `Cargo.lock`
    13#[derive(Serialize, Deserialize, Debug)]
    14#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
    15pub struct TomlLockfile {
    16    /// The lockfile format version (`version =` field).
    17    ///
    18    /// This field is optional for backward compatibility. Older lockfiles, i.e. V1 and V2, does
    19    /// not have the version field serialized.
    20    pub version: Option<u32>,
    21    /// The list of `[[package]]` entries describing each resolved dependency.
    22    pub package: Option<Vec<TomlLockfileDependency>>,
    23    /// The `[root]` table describing the root package.
    24    ///
    25    /// This field is optional for backward compatibility. Older lockfiles have the root package
    26    /// separated, whereas newer lockfiles have the root package as part of `[[package]]`.
    27    pub root: Option<TomlLockfileDependency>,
    28    /// The `[metadata]` table
    29    ///
    30    ///
    31    /// In older lockfile versions, dependency checksums were stored here instead of alongside each
    32    /// package entry.
    33    pub metadata: Option<TomlLockfileMetadata>,
    34    /// The `[patch]` table describing unused patches.
    35    ///
    36    /// The lockfile stores them as a list of `[[patch.unused]]` entries.
    37    #[serde(default, skip_serializing_if = "TomlLockfilePatch::is_empty")]
    38    pub patch: TomlLockfilePatch,
    39}
    40
    41/// Serialization of lockfiles metadata
    42///
    43/// Older versions of lockfiles have their dependencies' checksums on this `[metadata]` table.
    44pub type TomlLockfileMetadata = BTreeMap<String, String>;
    45
    46/// Serialization of unused patches
    47///
    48/// Cargo stores patches that were declared but not used during resolution.
    49#[derive(Serialize, Deserialize, Debug, Default)]
    50#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
    51pub struct TomlLockfilePatch {
    52    /// The list of unused dependency patches.
    53    pub unused: Vec<TomlLockfileDependency>,
    54}
    55
    56impl TomlLockfilePatch {
    57    fn is_empty(&self) -> bool {
    58        self.unused.is_empty()
    59    }
    60}
    61
    62/// Serialization of lockfiles dependencies
    63#[derive(Serialize, Deserialize, Debug, PartialOrd, Ord, PartialEq, Eq)]
    64#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
    65pub struct TomlLockfileDependency {
    66    /// The name of the dependency.
    67    pub name: String,
    68    /// The version of the dependency.
    69    pub version: String,
    70    /// The source of the dependency.
    71    ///
    72    /// Cargo does not serialize path dependencies.
    73    pub source: Option<TomlLockfileSourceId>,
    74    /// The checksum of the dependency.
    75    ///
    76    /// In older lockfiles, checksums were not stored here and instead on a separate `[metadata]`
    77    /// table (see [`TomlLockfileMetadata`]).
    78    pub checksum: Option<String>,
    79    /// The transitive dependencies used by this dependency.
    80    pub dependencies: Option<Vec<TomlLockfilePackageId>>,
    81    /// The replace of the dependency.
    82    pub replace: Option<TomlLockfilePackageId>,
    83}
    84
    85/// Serialization of dependency's source
    86#[derive(Debug, Clone)]
    87#[cfg_attr(
    88    feature = "unstable-schema",
    89    derive(schemars::JsonSchema),
    90    schemars(with = "String")
    91)]
    92pub struct TomlLockfileSourceId {
    93    /// The string representation of the source as it appears in the lockfile.
    94    source_str: String,
    95    /// The parsed source type, e.g. `git`, `registry`.
    96    ///
    97    /// Used for sources ordering.
    98    kind: SourceKind,
    99    /// The parsed URL of the source.
    100    ///
    101    /// Used for sources ordering.
    102    url: Url,
    103}
    104
    105impl TomlLockfileSourceId {
    106    pub fn new(source: String) -> Result<Self, TomlLockfileSourceIdError> {
    107        let source_str = source.clone();
    108        let (kind, url) = source
    109            .split_once('+')
    110            .ok_or_else(|| TomlLockfileSourceIdErrorKind::InvalidSource(source.clone()))?;
    111
    112        // Sparse URLs store the kind prefix (sparse+) in the URL. Therefore, for sparse kinds, we
    113        // want to use the raw `source` instead of the split `url`.
    114        let url = Url::parse(if kind == "sparse" { &source } else { url }).map_err(|msg| {
    115            TomlLockfileSourceIdErrorKind::InvalidUrl {
    116                url: url.to_string(),
    117                msg: msg.to_string(),
    118            }
    119        })?;
    120
    121        let kind = match kind {
    122            "git" => {
    123                let reference = GitReference::from_query(url.query_pairs());
    124                SourceKind::Git(reference)
    125            }
    126            "registry" => SourceKind::Registry,
    127            "sparse" => SourceKind::SparseRegistry,
    128            "path" => SourceKind::Path,
    129            kind => {
    130                return Err(
    131                    TomlLockfileSourceIdErrorKind::UnsupportedSource(kind.to_string()).into(),
    132                );
    133            }
    134        };
    135
    136        Ok(Self {
    137            source_str,
    138            kind,
    139            url,
    140        })
    141    }
    142
    143    pub fn kind(&self) -> &SourceKind {
    144        &self.kind
    145    }
    146
    147    pub fn url(&self) -> &Url {
    148        &self.url
    149    }
    150
    151    pub fn source_str(&self) -> &String {
    152        &self.source_str
    153    }
    154
    155    pub fn as_url(&self) -> impl fmt::Display + '_ {
    156        self.source_str.clone()
    157    }
    158}
    159
    160impl ser::Serialize for TomlLockfileSourceId {
    161    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    162    where
    163        S: ser::Serializer,
    164    {
    165        s.collect_str(&self.as_url())
    166    }
    167}
    168
    169impl<'de> de::Deserialize<'de> for TomlLockfileSourceId {
    170    fn deserialize<D>(d: D) -> Result<Self, D::Error>
    171    where
    172        D: de::Deserializer<'de>,
    173    {
    174        let s = String::deserialize(d)?;
    175        Ok(TomlLockfileSourceId::new(s).map_err(de::Error::custom)?)
    176    }
    177}
    178
    179impl std::hash::Hash for TomlLockfileSourceId {
    180    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    181        self.kind.hash(state);
    182        self.url.hash(state);
    183    }
    184}
    185
    186impl std::cmp::PartialEq for TomlLockfileSourceId {
    187    fn eq(&self, other: &Self) -> bool {
    188        self.kind == other.kind && self.url == other.url
    189    }
    190}
    191
    192impl std::cmp::Eq for TomlLockfileSourceId {}
    193
    194impl PartialOrd for TomlLockfileSourceId {
    195    fn partial_cmp(&self, other: &TomlLockfileSourceId) -> Option<Ordering> {
    196        Some(self.cmp(other))
    197    }
    198}
    199
    200impl Ord for TomlLockfileSourceId {
    201    fn cmp(&self, other: &TomlLockfileSourceId) -> Ordering {
    202        self.kind
    203            .cmp(&other.kind)
    204            .then_with(|| self.url.cmp(&other.url))
    205    }
    206}
    207
    208/// Serialization of package IDs.
    209///
    210/// The version and source are only included when necessary to disambiguate between packages:
    211/// - If multiple packages share the same name, the version is included.
    212/// - If multiple packages share the same name and version, the source is included.
    213#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone)]
    214#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
    215pub struct TomlLockfilePackageId {
    216    pub name: String,
    217    pub version: Option<String>,
    218    pub source: Option<TomlLockfileSourceId>,
    219}
    220
    221impl fmt::Display for TomlLockfilePackageId {
    222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    223        write!(f, "{}", self.name)?;
    224        if let Some(s) = &self.version {
    225            write!(f, " {}", s)?;
    226        }
    227        if let Some(s) = &self.source {
    228            write!(f, " ({})", s.as_url())?;
    229        }
    230        Ok(())
    231    }
    232}
    233
    234impl FromStr for TomlLockfilePackageId {
    235    type Err = TomlLockfilePackageIdError;
    236
    237    fn from_str(s: &str) -> Result<TomlLockfilePackageId, Self::Err> {
    238        let mut s = s.splitn(3, ' ');
    239        let name = s.next().unwrap();
    240        let version = s.next();
    241        let source_id = match s.next() {
    242            Some(s) => {
    243                if let Some(s) = s.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
    244                    Some(TomlLockfileSourceId::new(s.to_string())?)
    245                } else {
    246                    return Err(TomlLockfilePackageIdErrorKind::InvalidSerializedPackageId.into());
    247                }
    248            }
    249            None => None,
    250        };
    251
    252        Ok(TomlLockfilePackageId {
    253            name: name.to_string(),
    254            version: version.map(|v| v.to_string()),
    255            source: source_id,
    256        })
    257    }
    258}
    259
    260impl ser::Serialize for TomlLockfilePackageId {
    261    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    262    where
    263        S: ser::Serializer,
    264    {
    265        s.collect_str(self)
    266    }
    267}
    268
    269impl<'de> de::Deserialize<'de> for TomlLockfilePackageId {
    270    fn deserialize<D>(d: D) -> Result<TomlLockfilePackageId, D::Error>
    271    where
    272        D: de::Deserializer<'de>,
    273    {
    274        String::deserialize(d).and_then(|string| {
    275            string
    276                .parse::<TomlLockfilePackageId>()
    277                .map_err(de::Error::custom)
    278        })
    279    }
    280}
    281
    282#[derive(Debug, thiserror::Error)]
    283#[error(transparent)]
    284pub struct TomlLockfileSourceIdError(#[from] TomlLockfileSourceIdErrorKind);
    285
    286#[non_exhaustive]
    287#[derive(Debug, thiserror::Error)]
    288enum TomlLockfileSourceIdErrorKind {
    289    #[error("invalid source `{0}`")]
    290    InvalidSource(String),
    291
    292    #[error("invalid url `{url}`: {msg}")]
    293    InvalidUrl { url: String, msg: String },
    294
    295    #[error("unsupported source protocol: {0}")]
    296    UnsupportedSource(String),
    297}
    298
    299#[derive(Debug, thiserror::Error)]
    300#[error(transparent)]
    301pub struct TomlLockfilePackageIdError(#[from] TomlLockfilePackageIdErrorKind);
    302
    303impl From<TomlLockfileSourceIdError> for TomlLockfilePackageIdError {
    304    fn from(value: TomlLockfileSourceIdError) -> Self {
    305        TomlLockfilePackageIdErrorKind::Source(value).into()
    306    }
    307}
    308
    309#[non_exhaustive]
    310#[derive(Debug, thiserror::Error)]
    311enum TomlLockfilePackageIdErrorKind {
    312    #[error("invalid serialied PackageId")]
    313    InvalidSerializedPackageId,
    314
    315    #[error(transparent)]
    316    Source(#[from] TomlLockfileSourceIdError),
    317}
    318
    319#[cfg(feature = "unstable-schema")]
    320#[test]
    321fn dump_lockfile_schema() {
    322    let schema = schemars::schema_for!(crate::lockfile::TomlLockfile);
    323    let dump = serde_json::to_string_pretty(&schema).unwrap();
    324    snapbox::assert_data_eq!(dump, snapbox::file!("../lockfile.schema.json").raw());
    325}
    326
    327#[cfg(test)]
    328mod tests {
    329    use crate::core::{GitReference, SourceKind};
    330    use crate::lockfile::{TomlLockfileSourceId, TomlLockfileSourceIdErrorKind};
    331
    332    #[track_caller]
    333    fn ok(source_str: &str, source_kind: SourceKind, url: &str) {
    334        let source_str = source_str.to_owned();
    335        let source_id = TomlLockfileSourceId::new(source_str).unwrap();
    336        assert_eq!(source_id.kind, source_kind);
    337        assert_eq!(source_id.url().to_string(), url);
    338    }
    339
    340    macro_rules! err {
    341        ($src:expr, $expected:pat) => {
    342            let kind = TomlLockfileSourceId::new($src.to_owned()).unwrap_err().0;
    343            assert!(
    344                matches!(kind, $expected),
    345                "`{}` parse error mismatch, got {kind:?}",
    346                $src,
    347            );
    348        };
    349    }
    350
    351    #[test]
    352    fn good_sources() {
    353        ok(
    354            "sparse+https://my-crates.io",
    355            SourceKind::SparseRegistry,
    356            "sparse+https://my-crates.io",
    357        );
    358        ok(
    359            "registry+https://github.com/rust-lang/crates.io-index",
    360            SourceKind::Registry,
    361            "https://github.com/rust-lang/crates.io-index",
    362        );
    363        ok(
    364            "git+https://github.com/rust-lang/cargo",
    365            SourceKind::Git(GitReference::DefaultBranch),
    366            "https://github.com/rust-lang/cargo",
    367        );
    368        ok(
    369            "git+https://github.com/rust-lang/cargo?branch=dev",
    370            SourceKind::Git(GitReference::Branch("dev".to_owned())),
    371            "https://github.com/rust-lang/cargo?branch=dev",
    372        );
    373        ok(
    374            "git+https://github.com/rust-lang/cargo?tag=v1.0",
    375            SourceKind::Git(GitReference::Tag("v1.0".to_owned())),
    376            "https://github.com/rust-lang/cargo?tag=v1.0",
    377        );
    378        ok(
    379            "git+https://github.com/rust-lang/cargo?rev=refs/pull/493/head",
    380            SourceKind::Git(GitReference::Rev("refs/pull/493/head".to_owned())),
    381            "https://github.com/rust-lang/cargo?rev=refs/pull/493/head",
    382        );
    383        ok(
    384            "path+file:///path/to/root",
    385            SourceKind::Path,
    386            "file:///path/to/root",
    387        );
    388    }
    389
    390    #[test]
    391    fn bad_sources() {
    392        err!(
    393            "unknown+https://my-crates.io",
    394            TomlLockfileSourceIdErrorKind::UnsupportedSource(..)
    395        );
    396        err!(
    397            "registry+https//github.com/rust-lang/crates.io-index",
    398            TomlLockfileSourceIdErrorKind::InvalidUrl { .. }
    399        );
    400        err!(
    401            "https//github.com/rust-lang/crates.io-index",
    402            TomlLockfileSourceIdErrorKind::InvalidSource(..)
    403        );
    404    }
    405}