use crate::manifest::RustVersion; use semver::Version; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, collections::BTreeMap}; /// A single line in the index representing a single version of a package. #[derive(Deserialize, Serialize)] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] pub struct IndexPackage<'a> { /// Name of the package. #[serde(borrow)] pub name: Cow<'a, str>, /// The version of this dependency. pub vers: Version, /// All kinds of direct dependencies of the package, including dev and /// build dependencies. #[serde(borrow)] pub deps: Vec>, /// Set of features defined for the package, i.e., `[features]` table. #[serde(default)] pub features: BTreeMap, Vec>>, /// This field contains features with new, extended syntax. Specifically, /// namespaced features (`dep:`) and weak dependencies (`pkg?/feat`). /// /// This is separated from `features` because versions older than 1.19 /// will fail to load due to not being able to parse the new syntax, even /// with a `Cargo.lock` file. pub features2: Option, Vec>>>, /// Checksum for verifying the integrity of the corresponding downloaded package. pub cksum: String, /// If `true`, Cargo will skip this version when resolving. /// /// This was added in 2014. Everything in the crates.io index has this set /// now, so this probably doesn't need to be an option anymore. pub yanked: Option, /// Native library name this package links to. /// /// Added early 2018 (see ), /// can be `None` if published before then. pub links: Option>, /// Required version of rust /// /// Corresponds to `package.rust-version`. /// /// Added in 2023 (see ), /// can be `None` if published before then or if not set in the manifest. #[cfg_attr(feature = "unstable-schema", schemars(with = "Option"))] pub rust_version: Option, /// The publish time of this package version (optional). /// /// The format is a subset of ISO8601: /// - `yyyy-mm-ddThh:mm:ssZ` /// - no fractional seconds /// - always `Z` for UTC timezone, no timezone offsets supported /// - fields are 0-padded /// /// Example: 2025-11-12T19:30:12Z /// /// This should be the original publish time and not changed on any status changes, /// like [`IndexPackage::yanked`]. #[cfg_attr(feature = "unstable-schema", schemars(with = "Option"))] #[serde(with = "serde_pubtime")] #[serde(default)] pub pubtime: Option, /// The schema version for this entry. /// /// If this is None, it defaults to version `1`. Entries with unknown /// versions are ignored. /// /// Version `2` schema adds the `features2` field. /// /// Version `3` schema adds `artifact`, `bindep_targes`, and `lib` for /// artifact dependencies support. /// /// This provides a method to safely introduce changes to index entries /// and allow older versions of cargo to ignore newer entries it doesn't /// understand. This is honored as of 1.51, so unfortunately older /// versions will ignore it, and potentially misinterpret version 2 and /// newer entries. /// /// The intent is that versions older than 1.51 will work with a /// pre-existing `Cargo.lock`, but they may not correctly process `cargo /// update` or build a lock from scratch. In that case, cargo may /// incorrectly select a new package that uses a new index schema. A /// workaround is to downgrade any packages that are incompatible with the /// `--precise` flag of `cargo update`. pub v: Option, } /// A dependency as encoded in the [`IndexPackage`] index JSON. #[derive(Deserialize, Serialize, Clone)] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] pub struct RegistryDependency<'a> { /// Name of the dependency. If the dependency is renamed, the original /// would be stored in [`RegistryDependency::package`]. #[serde(borrow)] pub name: Cow<'a, str>, /// The SemVer requirement for this dependency. #[serde(borrow)] pub req: Cow<'a, str>, /// Set of features enabled for this dependency. #[serde(default)] pub features: Vec>, /// Whether or not this is an optional dependency. #[serde(default)] pub optional: bool, /// Whether or not default features are enabled. #[serde(default = "default_true")] pub default_features: bool, /// The target platform for this dependency. pub target: Option>, /// The dependency kind. "dev", "build", and "normal". pub kind: Option>, /// The URL of the index of the registry where this dependency is from. /// `None` if it is from the same index. pub registry: Option>, /// The original name if the dependency is renamed. pub package: Option>, /// Whether or not this is a public dependency. Unstable. See [RFC 1977]. /// /// [RFC 1977]: https://rust-lang.github.io/rfcs/1977-public-private-dependencies.html pub public: Option, /// The artifacts to build from this dependency. pub artifact: Option>>, /// The target for bindep. pub bindep_target: Option>, /// Whether or not this is a library dependency. #[serde(default)] pub lib: bool, } pub fn parse_pubtime(s: &str) -> Result { let dt = jiff::civil::DateTime::strptime("%Y-%m-%dT%H:%M:%SZ", s)?; if s.len() == 20 { let zoned = dt.to_zoned(jiff::tz::TimeZone::UTC)?; let timestamp = zoned.timestamp(); Ok(timestamp) } else { Err(jiff::Error::from_args(format_args!( "padding required for `{s}`" ))) } } pub fn format_pubtime(t: jiff::Timestamp) -> String { t.strftime("%Y-%m-%dT%H:%M:%SZ").to_string() } mod serde_pubtime { #[inline] pub(super) fn serialize( timestamp: &Option, se: S, ) -> Result { match *timestamp { None => se.serialize_none(), Some(ref ts) => { let s = super::format_pubtime(*ts); se.serialize_str(&s) } } } #[inline] pub(super) fn deserialize<'de, D: serde::Deserializer<'de>>( de: D, ) -> Result, D::Error> { de.deserialize_option(OptionalVisitor( serde_untagged::UntaggedEnumVisitor::new() .expecting("date time") .string(|value| super::parse_pubtime(&value).map_err(serde::de::Error::custom)), )) } /// A generic visitor for `Option`. struct OptionalVisitor(V); impl<'de, V: serde::de::Visitor<'de, Value = jiff::Timestamp>> serde::de::Visitor<'de> for OptionalVisitor { type Value = Option; fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str("date time") } #[inline] fn visit_some>( self, de: D, ) -> Result, D::Error> { de.deserialize_str(self.0).map(Some) } #[inline] fn visit_none(self) -> Result, E> { Ok(None) } } } fn default_true() -> bool { true } #[test] fn escaped_char_in_index_json_blob() { let _: IndexPackage<'_> = serde_json::from_str( r#"{"name":"a","vers":"0.0.1","deps":[],"cksum":"bae3","features":{}}"#, ) .unwrap(); let _: IndexPackage<'_> = serde_json::from_str( r#"{"name":"a","vers":"0.0.1","deps":[],"cksum":"bae3","features":{"test":["k","q"]},"links":"a-sys"}"# ).unwrap(); // Now we add escaped cher all the places they can go // these are not valid, but it should error later than json parsing let _: IndexPackage<'_> = serde_json::from_str( r#"{ "name":"This name has a escaped cher in it \n\t\" ", "vers":"0.0.1", "deps":[{ "name": " \n\t\" ", "req": " \n\t\" ", "features": [" \n\t\" "], "optional": true, "default_features": true, "target": " \n\t\" ", "kind": " \n\t\" ", "registry": " \n\t\" " }], "cksum":"bae3", "features":{"test \n\t\" ":["k \n\t\" ","q \n\t\" "]}, "links":" \n\t\" "}"#, ) .unwrap(); } #[cfg(feature = "unstable-schema")] #[test] fn dump_index_schema() { let schema = schemars::schema_for!(crate::index::IndexPackage<'_>); let dump = serde_json::to_string_pretty(&schema).unwrap(); snapbox::assert_data_eq!(dump, snapbox::file!("../index.schema.json").raw()); } /// The [`config.json`] file stored in the index. /// /// The config file may look like: /// /// ```json /// { /// "dl": "https://example.com/api/{crate}/{version}/download", /// "api": "https://example.com/api", /// "auth-required": false /// } /// ``` /// /// [`config.json`]: https://doc.rust-lang.org/nightly/cargo/reference/registry-index.html#index-configuration #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] pub struct RegistryConfig { /// Download endpoint for all crates. /// /// The string is a template which will generate the download URL for the /// tarball of a specific version of a crate. The substrings `{crate}` and /// `{version}` will be replaced with the crate's name and version /// respectively. The substring `{prefix}` will be replaced with the /// crate's prefix directory name, and the substring `{lowerprefix}` will /// be replaced with the crate's prefix directory name converted to /// lowercase. The substring `{sha256-checksum}` will be replaced with the /// crate's sha256 checksum. /// /// For backwards compatibility, if the string does not contain any /// markers (`{crate}`, `{version}`, `{prefix}`, or `{lowerprefix}`), it /// will be extended with `/{crate}/{version}/download` to /// support registries like crates.io which were created before the /// templating setup was created. /// /// For more on the template of the download URL, see [Index Configuration]( /// https://doc.rust-lang.org/nightly/cargo/reference/registry-index.html#index-configuration). pub dl: String, /// API endpoint for the registry. This is what's actually hit to perform /// operations like yanks, owner modifications, publish new crates, etc. /// If this is None, the registry does not support API commands. pub api: Option, /// Whether all operations require authentication. See [RFC 3139]. /// /// [RFC 3139]: https://rust-lang.github.io/rfcs/3139-cargo-alternative-registry-auth.html #[serde(default)] pub auth_required: bool, } impl RegistryConfig { /// File name of [`RegistryConfig`]. pub const NAME: &'static str = "config.json"; } #[cfg(feature = "unstable-schema")] #[test] fn dump_registry_schema() { let schema = schemars::schema_for!(crate::index::RegistryConfig); let dump = serde_json::to_string_pretty(&schema).unwrap(); snapbox::assert_data_eq!(dump, snapbox::file!("../registry_config.schema.json").raw()); } #[test] fn pubtime_format() { use snapbox::str; let input = [ ("2025-11-12T19:30:12Z", Some(str!["2025-11-12T19:30:12Z"])), // Padded values ("2025-01-02T09:03:02Z", Some(str!["2025-01-02T09:03:02Z"])), // Alt timezone format ("2025-11-12T19:30:12-04", None), // Alt date/time separator ("2025-11-12 19:30:12Z", None), // Non-padded values ("2025-11-12T19:30:12+4", None), ("2025-1-12T19:30:12+4", None), ("2025-11-2T19:30:12+4", None), ("2025-11-12T9:30:12Z", None), ("2025-11-12T19:3:12Z", None), ("2025-11-12T19:30:2Z", None), ]; for (input, expected) in input { let (parsed, expected) = match (parse_pubtime(input), expected) { (Ok(_), None) => { panic!("`{input}` did not error"); } (Ok(parsed), Some(expected)) => (parsed, expected), (Err(err), Some(_)) => { panic!("`{input}` did not parse successfully: {err}"); } _ => { continue; } }; let output = format_pubtime(parsed); snapbox::assert_data_eq!(output, expected); } }