1use crate::manifest::RustVersion;
2use semver::Version;
3use serde::{Deserialize, Serialize};
4use std::{borrow::Cow, collections::BTreeMap};
5
6#[derive(Deserialize, Serialize)]
8#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
9pub struct IndexPackage<'a> {
10 #[serde(borrow)]
12 pub name: Cow<'a, str>,
13 pub vers: Version,
15 #[serde(borrow)]
18 pub deps: Vec<RegistryDependency<'a>>,
19 #[serde(default)]
21 pub features: BTreeMap<Cow<'a, str>, Vec<Cow<'a, str>>>,
22 pub features2: Option<BTreeMap<Cow<'a, str>, Vec<Cow<'a, str>>>>,
29 pub cksum: String,
31 pub yanked: Option<bool>,
36 pub links: Option<Cow<'a, str>>,
41 #[cfg_attr(feature = "unstable-schema", schemars(with = "Option<String>"))]
48 pub rust_version: Option<RustVersion>,
49 #[cfg_attr(feature = "unstable-schema", schemars(with = "Option<String>"))]
56 #[serde(with = "serde_pubtime")]
57 #[serde(default)]
58 pub pubtime: Option<jiff::Timestamp>,
59 pub v: Option<u32>,
82}
83
84#[derive(Deserialize, Serialize, Clone)]
86#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
87pub struct RegistryDependency<'a> {
88 #[serde(borrow)]
91 pub name: Cow<'a, str>,
92 #[serde(borrow)]
94 pub req: Cow<'a, str>,
95 #[serde(default)]
97 pub features: Vec<Cow<'a, str>>,
98 #[serde(default)]
100 pub optional: bool,
101 #[serde(default = "default_true")]
103 pub default_features: bool,
104 pub target: Option<Cow<'a, str>>,
106 pub kind: Option<Cow<'a, str>>,
108 pub registry: Option<Cow<'a, str>>,
111 pub package: Option<Cow<'a, str>>,
113 pub public: Option<bool>,
117 pub artifact: Option<Vec<Cow<'a, str>>>,
119 pub bindep_target: Option<Cow<'a, str>>,
121 #[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 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 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 ("2025-01-02T09:03:02Z", Some(str!["2025-01-02T09:03:02Z"])),
249 ("2025-11-12T19:30:12-04", None),
251 ("2025-11-12 19:30:12Z", None),
253 ("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}