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}