#
cargo_util_schemas/
index.rs

    1use crate::manifest::RustVersion;
    2use semver::Version;
    3use serde::{Deserialize, Serialize};
    4use std::{borrow::Cow, collections::BTreeMap};
    5
    6/// A single line in the index representing a single version of a package.
    7#[derive(Deserialize, Serialize)]
    8#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
    9pub struct IndexPackage<'a> {
    10    /// Name of the package.
    11    #[serde(borrow)]
    12    pub name: Cow<'a, str>,
    13    /// The version of this dependency.
    14    pub vers: Version,
    15    /// All kinds of direct dependencies of the package, including dev and
    16    /// build dependencies.
    17    #[serde(borrow)]
    18    pub deps: Vec<RegistryDependency<'a>>,
    19    /// Set of features defined for the package, i.e., `[features]` table.
    20    #[serde(default)]
    21    pub features: BTreeMap<Cow<'a, str>, Vec<Cow<'a, str>>>,
    22    /// This field contains features with new, extended syntax. Specifically,
    23    /// namespaced features (`dep:`) and weak dependencies (`pkg?/feat`).
    24    ///
    25    /// This is separated from `features` because versions older than 1.19
    26    /// will fail to load due to not being able to parse the new syntax, even
    27    /// with a `Cargo.lock` file.
    28    pub features2: Option<BTreeMap<Cow<'a, str>, Vec<Cow<'a, str>>>>,
    29    /// Checksum for verifying the integrity of the corresponding downloaded package.
    30    pub cksum: String,
    31    /// If `true`, Cargo will skip this version when resolving.
    32    ///
    33    /// This was added in 2014. Everything in the crates.io index has this set
    34    /// now, so this probably doesn't need to be an option anymore.
    35    pub yanked: Option<bool>,
    36    /// Native library name this package links to.
    37    ///
    38    /// Added early 2018 (see <https://github.com/rust-lang/cargo/pull/4978>),
    39    /// can be `None` if published before then.
    40    pub links: Option<Cow<'a, str>>,
    41    /// Required version of rust
    42    ///
    43    /// Corresponds to `package.rust-version`.
    44    ///
    45    /// Added in 2023 (see <https://github.com/rust-lang/crates.io/pull/6267>),
    46    /// can be `None` if published before then or if not set in the manifest.
    47    #[cfg_attr(feature = "unstable-schema", schemars(with = "Option<String>"))]
    48    pub rust_version: Option<RustVersion>,
    49    /// The publish time of this package version (optional).
    50    ///
    51    /// The format is a subset of ISO8601:
    52    /// - `yyyy-mm-ddThh:mm:ssZ`
    53    /// - no fractional seconds
    54    /// - always `Z` for UTC timezone, no timezone offsets supported
    55    /// - fields are 0-padded
    56    ///
    57    /// Example: 2025-11-12T19:30:12Z
    58    ///
    59    /// This should be the original publish time and not changed on any status changes,
    60    /// like [`IndexPackage::yanked`].
    61    #[cfg_attr(feature = "unstable-schema", schemars(with = "Option<String>"))]
    62    #[serde(with = "serde_pubtime")]
    63    #[serde(default)]
    64    pub pubtime: Option<jiff::Timestamp>,
    65    /// The schema version for this entry.
    66    ///
    67    /// If this is None, it defaults to version `1`. Entries with unknown
    68    /// versions are ignored.
    69    ///
    70    /// Version `2` schema adds the `features2` field.
    71    ///
    72    /// Version `3` schema adds `artifact`, `bindep_targes`, and `lib` for
    73    /// artifact dependencies support.
    74    ///
    75    /// This provides a method to safely introduce changes to index entries
    76    /// and allow older versions of cargo to ignore newer entries it doesn't
    77    /// understand. This is honored as of 1.51, so unfortunately older
    78    /// versions will ignore it, and potentially misinterpret version 2 and
    79    /// newer entries.
    80    ///
    81    /// The intent is that versions older than 1.51 will work with a
    82    /// pre-existing `Cargo.lock`, but they may not correctly process `cargo
    83    /// update` or build a lock from scratch. In that case, cargo may
    84    /// incorrectly select a new package that uses a new index schema. A
    85    /// workaround is to downgrade any packages that are incompatible with the
    86    /// `--precise` flag of `cargo update`.
    87    pub v: Option<u32>,
    88}
    89
    90/// A dependency as encoded in the [`IndexPackage`] index JSON.
    91#[derive(Deserialize, Serialize, Clone)]
    92#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
    93pub struct RegistryDependency<'a> {
    94    /// Name of the dependency. If the dependency is renamed, the original
    95    /// would be stored in [`RegistryDependency::package`].
    96    #[serde(borrow)]
    97    pub name: Cow<'a, str>,
    98    /// The SemVer requirement for this dependency.
    99    #[serde(borrow)]
    100    pub req: Cow<'a, str>,
    101    /// Set of features enabled for this dependency.
    102    #[serde(default)]
    103    pub features: Vec<Cow<'a, str>>,
    104    /// Whether or not this is an optional dependency.
    105    #[serde(default)]
    106    pub optional: bool,
    107    /// Whether or not default features are enabled.
    108    #[serde(default = "default_true")]
    109    pub default_features: bool,
    110    /// The target platform for this dependency.
    111    pub target: Option<Cow<'a, str>>,
    112    /// The dependency kind. "dev", "build", and "normal".
    113    pub kind: Option<Cow<'a, str>>,
    114    /// The URL of the index of the registry where this dependency is from.
    115    /// `None` if it is from the same index.
    116    pub registry: Option<Cow<'a, str>>,
    117    /// The original name if the dependency is renamed.
    118    pub package: Option<Cow<'a, str>>,
    119    /// Whether or not this is a public dependency. Unstable. See [RFC 1977].
    120    ///
    121    /// [RFC 1977]: https://rust-lang.github.io/rfcs/1977-public-private-dependencies.html
    122    pub public: Option<bool>,
    123    /// The artifacts to build from this dependency.
    124    pub artifact: Option<Vec<Cow<'a, str>>>,
    125    /// The target for bindep.
    126    pub bindep_target: Option<Cow<'a, str>>,
    127    /// Whether or not this is a library dependency.
    128    #[serde(default)]
    129    pub lib: bool,
    130}
    131
    132pub fn parse_pubtime(s: &str) -> Result<jiff::Timestamp, jiff::Error> {
    133    let dt = jiff::civil::DateTime::strptime("%Y-%m-%dT%H:%M:%SZ", s)?;
    134    if s.len() == 20 {
    135        let zoned = dt.to_zoned(jiff::tz::TimeZone::UTC)?;
    136        let timestamp = zoned.timestamp();
    137        Ok(timestamp)
    138    } else {
    139        Err(jiff::Error::from_args(format_args!(
    140            "padding required for `{s}`"
    141        )))
    142    }
    143}
    144
    145pub fn format_pubtime(t: jiff::Timestamp) -> String {
    146    t.strftime("%Y-%m-%dT%H:%M:%SZ").to_string()
    147}
    148
    149mod serde_pubtime {
    150    #[inline]
    151    pub(super) fn serialize<S: serde::Serializer>(
    152        timestamp: &Option<jiff::Timestamp>,
    153        se: S,
    154    ) -> Result<S::Ok, S::Error> {
    155        match *timestamp {
    156            None => se.serialize_none(),
    157            Some(ref ts) => {
    158                let s = super::format_pubtime(*ts);
    159                se.serialize_str(&s)
    160            }
    161        }
    162    }
    163
    164    #[inline]
    165    pub(super) fn deserialize<'de, D: serde::Deserializer<'de>>(
    166        de: D,
    167    ) -> Result<Option<jiff::Timestamp>, D::Error> {
    168        de.deserialize_option(OptionalVisitor(
    169            serde_untagged::UntaggedEnumVisitor::new()
    170                .expecting("date time")
    171                .string(|value| super::parse_pubtime(&value).map_err(serde::de::Error::custom)),
    172        ))
    173    }
    174
    175    /// A generic visitor for `Option<DateTime>`.
    176    struct OptionalVisitor<V>(V);
    177
    178    impl<'de, V: serde::de::Visitor<'de, Value = jiff::Timestamp>> serde::de::Visitor<'de>
    179        for OptionalVisitor<V>
    180    {
    181        type Value = Option<jiff::Timestamp>;
    182
    183        fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
    184            f.write_str("date time")
    185        }
    186
    187        #[inline]
    188        fn visit_some<D: serde::de::Deserializer<'de>>(
    189            self,
    190            de: D,
    191        ) -> Result<Option<jiff::Timestamp>, D::Error> {
    192            de.deserialize_str(self.0).map(Some)
    193        }
    194
    195        #[inline]
    196        fn visit_none<E: serde::de::Error>(self) -> Result<Option<jiff::Timestamp>, E> {
    197            Ok(None)
    198        }
    199    }
    200}
    201
    202fn default_true() -> bool {
    203    true
    204}
    205
    206#[test]
    207fn escaped_char_in_index_json_blob() {
    208    let _: IndexPackage<'_> = serde_json::from_str(
    209        r#"{"name":"a","vers":"0.0.1","deps":[],"cksum":"bae3","features":{}}"#,
    210    )
    211    .unwrap();
    212    let _: IndexPackage<'_> = serde_json::from_str(
    213        r#"{"name":"a","vers":"0.0.1","deps":[],"cksum":"bae3","features":{"test":["k","q"]},"links":"a-sys"}"#
    214    ).unwrap();
    215
    216    // Now we add escaped cher all the places they can go
    217    // these are not valid, but it should error later than json parsing
    218    let _: IndexPackage<'_> = serde_json::from_str(
    219        r#"{
    220        "name":"This name has a escaped cher in it \n\t\" ",
    221        "vers":"0.0.1",
    222        "deps":[{
    223            "name": " \n\t\" ",
    224            "req": " \n\t\" ",
    225            "features": [" \n\t\" "],
    226            "optional": true,
    227            "default_features": true,
    228            "target": " \n\t\" ",
    229            "kind": " \n\t\" ",
    230            "registry": " \n\t\" "
    231        }],
    232        "cksum":"bae3",
    233        "features":{"test \n\t\" ":["k \n\t\" ","q \n\t\" "]},
    234        "links":" \n\t\" "}"#,
    235    )
    236    .unwrap();
    237}
    238
    239#[cfg(feature = "unstable-schema")]
    240#[test]
    241fn dump_index_schema() {
    242    let schema = schemars::schema_for!(crate::index::IndexPackage<'_>);
    243    let dump = serde_json::to_string_pretty(&schema).unwrap();
    244    snapbox::assert_data_eq!(dump, snapbox::file!("../index.schema.json").raw());
    245}
    246
    247#[test]
    248fn pubtime_format() {
    249    use snapbox::str;
    250
    251    let input = [
    252        ("2025-11-12T19:30:12Z", Some(str!["2025-11-12T19:30:12Z"])),
    253        // Padded values
    254        ("2025-01-02T09:03:02Z", Some(str!["2025-01-02T09:03:02Z"])),
    255        // Alt timezone format
    256        ("2025-11-12T19:30:12-04", None),
    257        // Alt date/time separator
    258        ("2025-11-12 19:30:12Z", None),
    259        // Non-padded values
    260        ("2025-11-12T19:30:12+4", None),
    261        ("2025-1-12T19:30:12+4", None),
    262        ("2025-11-2T19:30:12+4", None),
    263        ("2025-11-12T9:30:12Z", None),
    264        ("2025-11-12T19:3:12Z", None),
    265        ("2025-11-12T19:30:2Z", None),
    266    ];
    267    for (input, expected) in input {
    268        let (parsed, expected) = match (parse_pubtime(input), expected) {
    269            (Ok(_), None) => {
    270                panic!("`{input}` did not error");
    271            }
    272            (Ok(parsed), Some(expected)) => (parsed, expected),
    273            (Err(err), Some(_)) => {
    274                panic!("`{input}` did not parse successfully: {err}");
    275            }
    276            _ => {
    277                continue;
    278            }
    279        };
    280        let output = format_pubtime(parsed);
    281        snapbox::assert_data_eq!(output, expected);
    282    }
    283}