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>"))]
62 #[serde(with = "serde_pubtime")]
63 #[serde(default)]
64 pub pubtime: Option<jiff::Timestamp>,
65 pub v: Option<u32>,
88}
89
90#[derive(Deserialize, Serialize, Clone)]
92#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
93pub struct RegistryDependency<'a> {
94 #[serde(borrow)]
97 pub name: Cow<'a, str>,
98 #[serde(borrow)]
100 pub req: Cow<'a, str>,
101 #[serde(default)]
103 pub features: Vec<Cow<'a, str>>,
104 #[serde(default)]
106 pub optional: bool,
107 #[serde(default = "default_true")]
109 pub default_features: bool,
110 pub target: Option<Cow<'a, str>>,
112 pub kind: Option<Cow<'a, str>>,
114 pub registry: Option<Cow<'a, str>>,
117 pub package: Option<Cow<'a, str>>,
119 pub public: Option<bool>,
123 pub artifact: Option<Vec<Cow<'a, str>>>,
125 pub bindep_target: Option<Cow<'a, str>>,
127 #[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 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 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 ("2025-01-02T09:03:02Z", Some(str!["2025-01-02T09:03:02Z"])),
255 ("2025-11-12T19:30:12-04", None),
257 ("2025-11-12 19:30:12Z", None),
259 ("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}