1use std::collections::BTreeMap;
2use std::fmt::{Display, Formatter, Write};
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7mod pubtime_format {
8 use chrono::{DateTime, Utc};
9 use serde::{self, Deserialize, Deserializer, Serializer};
10
11 const FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
12
13 #[allow(clippy::ref_option)] pub(super) fn serialize<S>(
15 date: &Option<DateTime<Utc>>,
16 serializer: S,
17 ) -> Result<S::Ok, S::Error>
18 where
19 S: Serializer,
20 {
21 match date {
22 Some(dt) => serializer.serialize_str(&dt.format(FORMAT).to_string()),
23 None => serializer.serialize_none(),
24 }
25 }
26
27 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
28 where
29 D: Deserializer<'de>,
30 {
31 let s: Option<String> = Option::deserialize(deserializer)?;
32 match s {
33 Some(s) => DateTime::parse_from_rfc3339(&s)
34 .map(|dt| Some(dt.with_timezone(&Utc)))
35 .map_err(serde::de::Error::custom),
36 None => Ok(None),
37 }
38 }
39}
40
41use crate::publish_metadata::{PublishMetadata, RegistryDep};
42
43#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
49pub struct IndexMetadata {
50 pub name: String,
53 pub vers: String,
57 pub deps: Vec<IndexDep>,
59 pub cksum: String,
61 pub features: BTreeMap<String, Vec<String>>,
68 pub yanked: bool,
70 #[serde(skip_serializing_if = "Option::is_none")]
73 pub links: Option<String>,
74 #[serde(
76 default,
77 skip_serializing_if = "Option::is_none",
78 with = "pubtime_format"
79 )]
80 pub pubtime: Option<DateTime<Utc>>,
81 #[serde(skip_serializing_if = "Option::is_none")]
99 pub v: Option<u32>,
100 #[serde(skip_serializing_if = "Option::is_none")]
121 pub features2: Option<BTreeMap<String, Vec<String>>>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub rust_version: Option<String>,
129}
130
131impl IndexMetadata {
132 pub fn to_json(&self) -> Result<String, serde_json::Error> {
133 serde_json::to_string(&self)
134 }
135
136 pub fn from_reg_meta(registry_metadata: &PublishMetadata, cksum: &str) -> Self {
137 let (features, features2) = split_features(registry_metadata.features.clone());
138 IndexMetadata {
139 name: registry_metadata.name.clone(),
140 vers: registry_metadata.vers.clone(),
141 deps: registry_metadata
142 .deps
143 .clone()
144 .into_iter()
145 .map(IndexDep::from)
146 .collect(),
147 cksum: cksum.to_string(),
148 pubtime: Some(Utc::now()),
149 v: Some(schema_version(features2.as_ref())),
150 features,
151 features2,
152 yanked: false,
153 links: registry_metadata.links.clone(),
154 rust_version: registry_metadata.rust_version.clone(),
155 }
156 }
157
158 pub fn minimal(name: &str, vers: &str, cksum: &str) -> Self {
159 Self {
160 name: name.to_string(),
161 vers: vers.to_string(),
162 cksum: cksum.to_string(),
163 deps: vec![],
164 features: BTreeMap::default(),
165 yanked: false,
166 links: None,
167 pubtime: None,
168 v: Some(1),
169 features2: None,
170 rust_version: None,
171 }
172 }
173
174 pub fn serialize_indices(indices: &[IndexMetadata]) -> Result<String, serde_json::Error> {
175 let mut index = String::new();
176 for ix in indices {
177 writeln!(&mut index, "{}", serde_json::to_string(ix)?).unwrap();
178 }
179 Ok(index)
180 }
181}
182
183pub type Features = BTreeMap<String, Vec<String>>;
185
186pub fn split_features(features: Features) -> (Features, Option<Features>) {
190 let (features2, features): (BTreeMap<_, _>, BTreeMap<_, _>) =
191 features.into_iter().partition(|(_, values)| {
192 values
193 .iter()
194 .any(|v| v.starts_with("dep:") || v.contains("?/"))
195 });
196 let features2 = if features2.is_empty() {
197 None
198 } else {
199 Some(features2)
200 };
201 (features, features2)
202}
203
204pub fn schema_version(features2: Option<&Features>) -> u32 {
206 if features2.is_some() { 2 } else { 1 }
207}
208
209#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
210pub struct IndexDep {
211 pub name: String,
216 pub req: String,
220 pub features: Vec<String>,
222 pub optional: bool,
224 pub default_features: bool,
226 pub target: Option<String>,
230 pub kind: Option<DependencyKind>,
236 pub registry: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
244 pub package: Option<String>,
245}
246
247#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
248pub enum DependencyKind {
249 Normal,
250 Build,
251 Dev,
252 Other(String),
253}
254
255impl<'de> Deserialize<'de> for DependencyKind {
256 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257 where
258 D: Deserializer<'de>,
259 {
260 let s = String::deserialize(deserializer)?;
261 match s.as_str() {
262 "normal" => Ok(DependencyKind::Normal),
263 "build" => Ok(DependencyKind::Build),
264 "dev" => Ok(DependencyKind::Dev),
265 _ => Ok(DependencyKind::Other(s)),
266 }
267 }
268}
269
270impl Serialize for DependencyKind {
271 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
272 where
273 S: Serializer,
274 {
275 match self {
276 DependencyKind::Normal => serializer.serialize_str("normal"),
277 DependencyKind::Build => serializer.serialize_str("build"),
278 DependencyKind::Dev => serializer.serialize_str("dev"),
279 DependencyKind::Other(s) => serializer.serialize_str(s),
280 }
281 }
282}
283
284impl Display for DependencyKind {
285 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
286 match self {
287 DependencyKind::Normal => write!(f, "normal"),
288 DependencyKind::Build => write!(f, "build"),
289 DependencyKind::Dev => write!(f, "dev"),
290 DependencyKind::Other(s) => write!(f, "{s}"),
291 }
292 }
293}
294
295impl From<String> for DependencyKind {
296 fn from(kind: String) -> Self {
297 match kind.as_str() {
298 "normal" => DependencyKind::Normal,
299 "build" => DependencyKind::Build,
300 "dev" => DependencyKind::Dev,
301 _ => DependencyKind::Other(kind),
302 }
303 }
304}
305
306impl From<RegistryDep> for IndexDep {
307 fn from(registry_dep: RegistryDep) -> Self {
308 IndexDep {
309 name: match registry_dep.explicit_name_in_toml {
310 Some(ref name) => name.clone(),
311 None => registry_dep.name.clone(),
312 },
313 req: registry_dep.version_req,
314 features: registry_dep.features.unwrap_or_default(),
315 optional: registry_dep.optional,
316 default_features: registry_dep.default_features,
317 target: registry_dep.target,
318 kind: registry_dep.kind.map(DependencyKind::from),
319 registry: registry_dep.registry,
320 package: match registry_dep.explicit_name_in_toml {
321 Some(_) => Some(registry_dep.name),
322 None => None,
323 },
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 fn features(entries: &[(&str, &[&str])]) -> BTreeMap<String, Vec<String>> {
333 entries
334 .iter()
335 .map(|(k, v)| {
336 (
337 (*k).to_string(),
338 v.iter().map(ToString::to_string).collect(),
339 )
340 })
341 .collect()
342 }
343
344 #[test]
345 fn new_feature_syntax_goes_into_features2() {
346 let reg_meta = PublishMetadata {
347 features: features(&[
348 ("default", &["std"]),
349 ("serde", &["dep:serde"]),
350 ("chrono", &["chrono?/serde", "std"]),
351 ]),
352 ..PublishMetadata::minimal("foo", "1.0.0")
353 };
354
355 let index = IndexMetadata::from_reg_meta(®_meta, "cksum");
356
357 assert_eq!(features(&[("default", &["std"])]), index.features);
358 assert_eq!(
359 Some(features(&[
360 ("chrono", &["chrono?/serde", "std"]),
361 ("serde", &["dep:serde"]),
362 ])),
363 index.features2
364 );
365 assert_eq!(Some(2), index.v);
366 }
367
368 #[test]
369 fn old_feature_syntax_stays_in_features() {
370 let reg_meta = PublishMetadata {
371 features: features(&[("default", &["std", "serde/std"])]),
372 ..PublishMetadata::minimal("foo", "1.0.0")
373 };
374
375 let index = IndexMetadata::from_reg_meta(®_meta, "cksum");
376
377 assert_eq!(reg_meta.features, index.features);
378 assert_eq!(None, index.features2);
379 assert_eq!(Some(1), index.v);
380 }
381
382 #[test]
383 fn transitive_dependency_rename() {
384 let reg_meta = PublishMetadata {
385 name: "foo".to_string(),
386 vers: "0.1.0".to_string(),
387 deps: vec![
388 RegistryDep {
389 name: "bar".to_string(),
390 version_req: "^0.1.0".to_string(),
391 features: None,
392 optional: false,
393 default_features: true,
394 target: None,
395 kind: None,
396 registry: None,
397 explicit_name_in_toml: None,
398 },
399 RegistryDep {
400 name: "baz".to_string(),
401 version_req: "^0.1.0".to_string(),
402 features: None,
403 optional: false,
404 default_features: true,
405 target: None,
406 kind: None,
407 registry: None,
408 explicit_name_in_toml: Some("qux".to_string()),
409 },
410 ],
411 features: BTreeMap::default(),
412 links: None,
413 description: None,
414 authors: None,
415 documentation: None,
416 homepage: None,
417 readme: None,
418 readme_file: None,
419 keywords: Vec::default(),
420 categories: Vec::default(),
421 license: None,
422 license_file: None,
423 repository: None,
424 badges: None,
425 rust_version: None,
426 };
427
428 let index_meta = IndexMetadata::from_reg_meta(®_meta, "1234");
429
430 assert_eq!(index_meta.deps.len(), 2);
431 assert_eq!(index_meta.deps[0].name, "bar");
432 assert_eq!(index_meta.deps[0].package, None);
433 assert_eq!(index_meta.deps[1].name, "qux");
434 assert_eq!(index_meta.deps[1].package, Some("baz".to_string()));
435 }
436
437 #[test]
438 fn pubtime_serializes_without_fractional_seconds() {
439 use chrono::TimeZone;
440
441 let pubtime = Utc.with_ymd_and_hms(2025, 1, 2, 9, 5, 7).unwrap();
442 let metadata = IndexMetadata {
443 name: "test".to_string(),
444 vers: "1.0.0".to_string(),
445 deps: vec![],
446 cksum: "abc123".to_string(),
447 features: BTreeMap::new(),
448 yanked: false,
449 links: None,
450 pubtime: Some(pubtime),
451 v: Some(1),
452 features2: None,
453 rust_version: None,
454 };
455
456 let json = metadata.to_json().unwrap();
457
458 assert!(
460 json.contains(r#""pubtime":"2025-01-02T09:05:07Z""#),
461 "Expected pubtime to be serialized as '2025-01-02T09:05:07Z', got: {json}"
462 );
463 }
464
465 #[test]
466 fn pubtime_none_is_omitted_from_serialization() {
467 let metadata = IndexMetadata {
468 name: "test".to_string(),
469 vers: "1.0.0".to_string(),
470 deps: vec![],
471 cksum: "abc123".to_string(),
472 features: BTreeMap::new(),
473 yanked: false,
474 links: None,
475 pubtime: None,
476 v: Some(1),
477 features2: None,
478 rust_version: None,
479 };
480
481 let json = metadata.to_json().unwrap();
482
483 assert!(
484 !json.contains("pubtime"),
485 "Expected pubtime to be omitted when None, got: {json}"
486 );
487 }
488
489 #[test]
490 fn pubtime_deserializes_from_rfc3339() {
491 use chrono::{Datelike, Timelike};
492
493 let json = r#"{"name":"test","vers":"1.0.0","deps":[],"cksum":"abc","features":{},"yanked":false,"pubtime":"2025-01-02T09:05:07Z","v":1}"#;
494
495 let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
496
497 assert!(metadata.pubtime.is_some());
498 let pubtime = metadata.pubtime.unwrap();
499 assert_eq!(pubtime.year(), 2025);
500 assert_eq!(pubtime.month(), 1);
501 assert_eq!(pubtime.day(), 2);
502 assert_eq!(pubtime.hour(), 9);
503 assert_eq!(pubtime.minute(), 5);
504 assert_eq!(pubtime.second(), 7);
505 }
506
507 #[test]
508 fn pubtime_deserializes_from_rfc3339_with_fractional_seconds() {
509 use chrono::{Datelike, Timelike};
510
511 let json = r#"{"name":"test","vers":"1.0.0","deps":[],"cksum":"abc","features":{},"yanked":false,"pubtime":"2025-01-02T09:05:07.123456Z","v":1}"#;
513
514 let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
515
516 assert!(metadata.pubtime.is_some());
517 let pubtime = metadata.pubtime.unwrap();
518 assert_eq!(pubtime.year(), 2025);
519 assert_eq!(pubtime.month(), 1);
520 assert_eq!(pubtime.day(), 2);
521 assert_eq!(pubtime.hour(), 9);
522 assert_eq!(pubtime.minute(), 5);
523 assert_eq!(pubtime.second(), 7);
524 }
525
526 #[test]
527 fn rust_version_round_trips_losslessly() {
528 let json = r#"{"name":"anyhow","vers":"1.0.100","deps":[],"cksum":"abc","features":{},"yanked":false,"v":2,"rust_version":"1.39"}"#;
531
532 let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
533
534 assert_eq!(metadata.rust_version, Some("1.39".to_string()));
535 assert_eq!(
536 serde_json::from_str::<serde_json::Value>(&metadata.to_json().unwrap()).unwrap(),
537 serde_json::from_str::<serde_json::Value>(json).unwrap()
538 );
539 }
540
541 #[test]
542 fn rust_version_none_is_omitted_from_serialization() {
543 let metadata = IndexMetadata::minimal("test", "1.0.0", "abc123");
544
545 let json = metadata.to_json().unwrap();
546
547 assert!(
548 !json.contains("rust_version"),
549 "Expected rust_version to be omitted when None, got: {json}"
550 );
551 }
552
553 #[test]
554 fn serialize_indices_ends_with_newline_and_one_line_per_entry() {
555 let mk = |vers: &str| IndexMetadata {
556 name: "crate".to_string(),
557 vers: vers.to_string(),
558 deps: vec![],
559 cksum: "cksum".to_string(),
560 features: BTreeMap::default(),
561 yanked: false,
562 links: None,
563 pubtime: None,
564 v: Some(1),
565 features2: None,
566 rust_version: None,
567 };
568 let indices = vec![mk("1.0.0"), mk("2.0.0")];
569
570 let out = IndexMetadata::serialize_indices(&indices).unwrap();
571
572 assert!(
578 out.ends_with('\n'),
579 "sparse index body must end with a newline, got: {out:?}"
580 );
581
582 let lines: Vec<&str> = out.lines().collect();
584 assert_eq!(lines.len(), indices.len());
585 for (line, meta) in lines.iter().zip(&indices) {
586 let parsed: IndexMetadata = serde_json::from_str(line).unwrap();
587 assert_eq!(parsed.vers, meta.vers);
588 }
589 }
590}