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