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/// The [`config.json`] file stored in the index.
248///
249/// The config file may look like:
250///
251/// ```json
252/// {
253/// "dl": "https://example.com/api/{crate}/{version}/download",
254/// "api": "https://example.com/api",
255/// "auth-required": false
256/// }
257/// ```
258///
259/// [`config.json`]: https://doc.rust-lang.org/nightly/cargo/reference/registry-index.html#index-configuration
260#[derive(Deserialize, Serialize, Debug, Clone)]
261#[serde(rename_all = "kebab-case")]
262#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
263pub struct RegistryConfig {
264 /// Download endpoint for all crates.
265 ///
266 /// The string is a template which will generate the download URL for the
267 /// tarball of a specific version of a crate. The substrings `{crate}` and
268 /// `{version}` will be replaced with the crate's name and version
269 /// respectively. The substring `{prefix}` will be replaced with the
270 /// crate's prefix directory name, and the substring `{lowerprefix}` will
271 /// be replaced with the crate's prefix directory name converted to
272 /// lowercase. The substring `{sha256-checksum}` will be replaced with the
273 /// crate's sha256 checksum.
274 ///
275 /// For backwards compatibility, if the string does not contain any
276 /// markers (`{crate}`, `{version}`, `{prefix}`, or `{lowerprefix}`), it
277 /// will be extended with `/{crate}/{version}/download` to
278 /// support registries like crates.io which were created before the
279 /// templating setup was created.
280 ///
281 /// For more on the template of the download URL, see [Index Configuration](
282 /// https://doc.rust-lang.org/nightly/cargo/reference/registry-index.html#index-configuration).
283 pub dl: String,
284
285 /// API endpoint for the registry. This is what's actually hit to perform
286 /// operations like yanks, owner modifications, publish new crates, etc.
287 /// If this is None, the registry does not support API commands.
288 pub api: Option<String>,
289
290 /// Whether all operations require authentication. See [RFC 3139].
291 ///
292 /// [RFC 3139]: https://rust-lang.github.io/rfcs/3139-cargo-alternative-registry-auth.html
293 #[serde(default)]
294 pub auth_required: bool,
295}
296
297impl RegistryConfig {
298 /// File name of [`RegistryConfig`].
299 pub const NAME: &'static str = "config.json";
300}
301
302#[cfg(feature = "unstable-schema")]
303#[test]
304fn dump_registry_schema() {
305 let schema = schemars::schema_for!(crate::index::RegistryConfig);
306 let dump = serde_json::to_string_pretty(&schema).unwrap();
307 snapbox::assert_data_eq!(dump, snapbox::file!("../registry_config.schema.json").raw());
308}
309
310#[test]
311fn pubtime_format() {
312 use snapbox::str;
313
314 let input = [
315 ("2025-11-12T19:30:12Z", Some(str!["2025-11-12T19:30:12Z"])),
316 // Padded values
317 ("2025-01-02T09:03:02Z", Some(str!["2025-01-02T09:03:02Z"])),
318 // Alt timezone format
319 ("2025-11-12T19:30:12-04", None),
320 // Alt date/time separator
321 ("2025-11-12 19:30:12Z", None),
322 // Non-padded values
323 ("2025-11-12T19:30:12+4", None),
324 ("2025-1-12T19:30:12+4", None),
325 ("2025-11-2T19:30:12+4", None),
326 ("2025-11-12T9:30:12Z", None),
327 ("2025-11-12T19:3:12Z", None),
328 ("2025-11-12T19:30:2Z", None),
329 ];
330 for (input, expected) in input {
331 let (parsed, expected) = match (parse_pubtime(input), expected) {
332 (Ok(_), None) => {
333 panic!("`{input}` did not error");
334 }
335 (Ok(parsed), Some(expected)) => (parsed, expected),
336 (Err(err), Some(_)) => {
337 panic!("`{input}` did not parse successfully: {err}");
338 }
339 _ => {
340 continue;
341 }
342 };
343 let output = format_pubtime(parsed);
344 snapbox::assert_data_eq!(output, expected);
345 }
346}