1use std::collections::BTreeMap;
2use std::fmt::{Display, Formatter, Write};
3use std::path::{Path, PathBuf};
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8mod pubtime_format {
9 use chrono::{DateTime, Utc};
10 use serde::{self, Deserialize, Deserializer, Serializer};
11
12 const FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
13
14 #[allow(clippy::ref_option)] pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
16 where
17 S: Serializer,
18 {
19 match date {
20 Some(dt) => serializer.serialize_str(&dt.format(FORMAT).to_string()),
21 None => serializer.serialize_none(),
22 }
23 }
24
25 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
26 where
27 D: Deserializer<'de>,
28 {
29 let s: Option<String> = Option::deserialize(deserializer)?;
30 match s {
31 Some(s) => DateTime::parse_from_rfc3339(&s)
32 .map(|dt| Some(dt.with_timezone(&Utc)))
33 .map_err(serde::de::Error::custom),
34 None => Ok(None),
35 }
36 }
37}
38use tokio::fs::File;
39use tokio::io::AsyncReadExt;
40
41use crate::publish_metadata::{PublishMetadata, RegistryDep};
42use crate::version::Version;
43
44#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
50pub struct IndexMetadata {
51 pub name: String,
54 pub vers: String,
58 pub deps: Vec<IndexDep>,
60 pub cksum: String,
62 pub features: BTreeMap<String, Vec<String>>,
69 pub yanked: bool,
71 #[serde(skip_serializing_if = "Option::is_none")]
74 pub links: Option<String>,
75 #[serde(
77 default,
78 skip_serializing_if = "Option::is_none",
79 with = "pubtime_format"
80 )]
81 pub pubtime: Option<DateTime<Utc>>,
82 #[serde(skip_serializing_if = "Option::is_none")]
100 pub v: Option<u32>,
101 #[serde(skip_serializing_if = "Option::is_none")]
122 pub features2: Option<BTreeMap<String, Vec<String>>>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub rust_version: Option<String>,
130}
131
132impl IndexMetadata {
133 pub async fn from_max_version(path: &Path) -> Result<Self, std::io::Error> {
134 let mut file = File::open(path).await?;
135 let mut content = String::new();
136 file.read_to_string(&mut content).await?;
137
138 let mut metadata: Vec<IndexMetadata> = content
139 .lines()
140 .filter_map(|m| serde_json::from_str::<IndexMetadata>(m).ok())
141 .collect();
142
143 metadata.sort_by(|a, b| {
144 let sv1 = Version::from_unchecked_str(&a.vers);
145 let sv2 = Version::from_unchecked_str(&b.vers);
146 sv1.cmp(&sv2)
147 });
148
149 metadata.last().cloned().ok_or_else(|| {
150 std::io::Error::new(
151 std::io::ErrorKind::InvalidData,
152 "Unable to read metadata file.",
153 )
154 })
155 }
156
157 pub async fn from_version(path: &Path, version: &Version) -> Result<Self, std::io::Error> {
158 let mut file = File::open(path).await?;
159 let mut content = String::new();
160 file.read_to_string(&mut content).await?;
161
162 let metadata: Vec<IndexMetadata> = content
163 .lines()
164 .filter_map(|m| serde_json::from_str::<IndexMetadata>(m).ok())
165 .collect();
166
167 metadata
168 .iter()
169 .find(|m| {
170 let sv = Version::try_from(&m.vers).unwrap_or_default();
171 sv == *version
172 })
173 .cloned()
174 .ok_or_else(|| {
175 std::io::Error::new(
176 std::io::ErrorKind::InvalidData,
177 "Unable to read metadata file.",
178 )
179 })
180 }
181
182 pub fn to_json(&self) -> Result<String, serde_json::Error> {
183 serde_json::to_string(&self)
184 }
185
186 pub fn metadata_path(&self, index_path: &Path) -> PathBuf {
187 metadata_path(index_path, &self.name)
188 }
189
190 pub fn from_reg_meta(registry_metadata: &PublishMetadata, cksum: &str) -> Self {
191 IndexMetadata {
192 name: registry_metadata.name.clone(),
193 vers: registry_metadata.vers.clone(),
194 deps: registry_metadata
195 .deps
196 .clone()
197 .into_iter()
198 .map(IndexDep::from)
199 .collect(),
200 cksum: cksum.to_string(),
201 pubtime: Some(Utc::now()),
202 features: registry_metadata.features.clone(),
203 yanked: false,
204 links: registry_metadata.links.clone(),
205 v: Some(1),
206 features2: None,
207 rust_version: registry_metadata.rust_version.clone(),
208 }
209 }
210
211 pub fn minimal(name: &str, vers: &str, cksum: &str) -> Self {
212 Self {
213 name: name.to_string(),
214 vers: vers.to_string(),
215 cksum: cksum.to_string(),
216 deps: vec![],
217 features: BTreeMap::default(),
218 yanked: false,
219 links: None,
220 pubtime: None,
221 v: Some(1),
222 features2: None,
223 rust_version: None,
224 }
225 }
226
227 pub fn serialize_indices(indices: &[IndexMetadata]) -> Result<String, serde_json::Error> {
228 let mut index = String::new();
229 for ix in indices {
230 writeln!(&mut index, "{}", serde_json::to_string(ix)?).unwrap();
231 }
232 Ok(index)
233 }
234}
235
236#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
237pub struct IndexDep {
238 pub name: String,
243 pub req: String,
247 pub features: Vec<String>,
249 pub optional: bool,
251 pub default_features: bool,
253 pub target: Option<String>,
257 pub kind: Option<DependencyKind>,
263 pub registry: Option<String>,
267 #[serde(skip_serializing_if = "Option::is_none")]
271 pub package: Option<String>,
272}
273
274#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
275pub enum DependencyKind {
276 Normal,
277 Build,
278 Dev,
279 Other(String),
280}
281
282impl<'de> Deserialize<'de> for DependencyKind {
283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284 where
285 D: Deserializer<'de>,
286 {
287 let s = String::deserialize(deserializer)?;
288 match s.as_str() {
289 "normal" => Ok(DependencyKind::Normal),
290 "build" => Ok(DependencyKind::Build),
291 "dev" => Ok(DependencyKind::Dev),
292 _ => Ok(DependencyKind::Other(s)),
293 }
294 }
295}
296
297impl Serialize for DependencyKind {
298 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
299 where
300 S: Serializer,
301 {
302 match self {
303 DependencyKind::Normal => serializer.serialize_str("normal"),
304 DependencyKind::Build => serializer.serialize_str("build"),
305 DependencyKind::Dev => serializer.serialize_str("dev"),
306 DependencyKind::Other(s) => serializer.serialize_str(s),
307 }
308 }
309}
310
311impl Display for DependencyKind {
312 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
313 match self {
314 DependencyKind::Normal => write!(f, "normal"),
315 DependencyKind::Build => write!(f, "build"),
316 DependencyKind::Dev => write!(f, "dev"),
317 DependencyKind::Other(s) => write!(f, "{s}"),
318 }
319 }
320}
321
322impl From<String> for DependencyKind {
323 fn from(kind: String) -> Self {
324 match kind.as_str() {
325 "normal" => DependencyKind::Normal,
326 "build" => DependencyKind::Build,
327 "dev" => DependencyKind::Dev,
328 _ => DependencyKind::Other(kind),
329 }
330 }
331}
332
333impl From<RegistryDep> for IndexDep {
334 fn from(registry_dep: RegistryDep) -> Self {
335 IndexDep {
336 name: match registry_dep.explicit_name_in_toml {
337 Some(ref name) => name.clone(),
338 None => registry_dep.name.clone(),
339 },
340 req: registry_dep.version_req,
341 features: registry_dep.features.unwrap_or_default(),
342 optional: registry_dep.optional,
343 default_features: registry_dep.default_features,
344 target: registry_dep.target,
345 kind: registry_dep.kind.map(DependencyKind::from),
346 registry: registry_dep.registry,
347 package: match registry_dep.explicit_name_in_toml {
348 Some(_) => Some(registry_dep.name),
349 None => None,
350 },
351 }
352 }
353}
354
355pub fn metadata_path(index_path: &Path, name: &str) -> PathBuf {
356 if name.len() == 1 {
357 index_path.join("1").join(name.to_lowercase())
358 } else if name.len() == 2 {
359 index_path.join("2").join(name.to_lowercase())
360 } else if name.len() == 3 {
361 let first_char = &name[0..1].to_lowercase();
362 index_path
363 .join("3")
364 .join(first_char)
365 .join(name.to_lowercase())
366 } else {
367 let first_two = &name[0..2].to_lowercase();
368 let second_two = &name[2..4].to_lowercase();
369 index_path
370 .join(first_two)
371 .join(second_two)
372 .join(name.to_lowercase())
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn transitive_dependency_rename() {
382 let reg_meta = PublishMetadata {
383 name: "foo".to_string(),
384 vers: "0.1.0".to_string(),
385 deps: vec![
386 RegistryDep {
387 name: "bar".to_string(),
388 version_req: "^0.1.0".to_string(),
389 features: None,
390 optional: false,
391 default_features: true,
392 target: None,
393 kind: None,
394 registry: None,
395 explicit_name_in_toml: None,
396 },
397 RegistryDep {
398 name: "baz".to_string(),
399 version_req: "^0.1.0".to_string(),
400 features: None,
401 optional: false,
402 default_features: true,
403 target: None,
404 kind: None,
405 registry: None,
406 explicit_name_in_toml: Some("qux".to_string()),
407 },
408 ],
409 features: BTreeMap::default(),
410 links: None,
411 description: None,
412 authors: None,
413 documentation: None,
414 homepage: None,
415 readme: None,
416 readme_file: None,
417 keywords: Vec::default(),
418 categories: Vec::default(),
419 license: None,
420 license_file: None,
421 repository: None,
422 badges: None,
423 rust_version: None,
424 };
425
426 let index_meta = IndexMetadata::from_reg_meta(®_meta, "1234");
427
428 assert_eq!(index_meta.deps.len(), 2);
429 assert_eq!(index_meta.deps[0].name, "bar");
430 assert_eq!(index_meta.deps[0].package, None);
431 assert_eq!(index_meta.deps[1].name, "qux");
432 assert_eq!(index_meta.deps[1].package, Some("baz".to_string()));
433 }
434
435 #[test]
436 fn metadata_path_one_letter() {
437 let name = "A";
438 assert_eq!(
439 metadata_path(&PathBuf::from("ip"), name),
440 Path::new("ip").join("1").join("a")
441 );
442 }
443
444 #[test]
445 fn metadata_path_two_letters() {
446 let name = "cB";
447 assert_eq!(
448 metadata_path(&PathBuf::from("ip"), name),
449 Path::new("ip").join("2").join("cb")
450 );
451 }
452
453 #[test]
454 fn metadata_path_three_letters() {
455 let name = "cAb";
456 assert_eq!(
457 metadata_path(&PathBuf::from("ip"), name),
458 Path::new("ip").join("3").join("c").join("cab")
459 );
460 }
461
462 #[test]
463 fn metadata_path_four_or_more_letters() {
464 let name = "foo_bAr";
465 assert_eq!(
466 metadata_path(&PathBuf::from("ip"), name),
467 Path::new("ip").join("fo").join("o_").join("foo_bar")
468 );
469 }
470
471 #[test]
472 fn pubtime_serializes_without_fractional_seconds() {
473 use chrono::TimeZone;
474
475 let pubtime = Utc.with_ymd_and_hms(2025, 1, 2, 9, 5, 7).unwrap();
476 let metadata = IndexMetadata {
477 name: "test".to_string(),
478 vers: "1.0.0".to_string(),
479 deps: vec![],
480 cksum: "abc123".to_string(),
481 features: BTreeMap::new(),
482 yanked: false,
483 links: None,
484 pubtime: Some(pubtime),
485 v: Some(1),
486 features2: None,
487 rust_version: None,
488 };
489
490 let json = metadata.to_json().unwrap();
491
492 assert!(
494 json.contains(r#""pubtime":"2025-01-02T09:05:07Z""#),
495 "Expected pubtime to be serialized as '2025-01-02T09:05:07Z', got: {json}"
496 );
497 }
498
499 #[test]
500 fn pubtime_none_is_omitted_from_serialization() {
501 let metadata = IndexMetadata {
502 name: "test".to_string(),
503 vers: "1.0.0".to_string(),
504 deps: vec![],
505 cksum: "abc123".to_string(),
506 features: BTreeMap::new(),
507 yanked: false,
508 links: None,
509 pubtime: None,
510 v: Some(1),
511 features2: None,
512 rust_version: None,
513 };
514
515 let json = metadata.to_json().unwrap();
516
517 assert!(
518 !json.contains("pubtime"),
519 "Expected pubtime to be omitted when None, got: {json}"
520 );
521 }
522
523 #[test]
524 fn pubtime_deserializes_from_rfc3339() {
525 use chrono::{Datelike, Timelike};
526
527 let json = r#"{"name":"test","vers":"1.0.0","deps":[],"cksum":"abc","features":{},"yanked":false,"pubtime":"2025-01-02T09:05:07Z","v":1}"#;
528
529 let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
530
531 assert!(metadata.pubtime.is_some());
532 let pubtime = metadata.pubtime.unwrap();
533 assert_eq!(pubtime.year(), 2025);
534 assert_eq!(pubtime.month(), 1);
535 assert_eq!(pubtime.day(), 2);
536 assert_eq!(pubtime.hour(), 9);
537 assert_eq!(pubtime.minute(), 5);
538 assert_eq!(pubtime.second(), 7);
539 }
540
541 #[test]
542 fn pubtime_deserializes_from_rfc3339_with_fractional_seconds() {
543 use chrono::{Datelike, Timelike};
544
545 let json = r#"{"name":"test","vers":"1.0.0","deps":[],"cksum":"abc","features":{},"yanked":false,"pubtime":"2025-01-02T09:05:07.123456Z","v":1}"#;
547
548 let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
549
550 assert!(metadata.pubtime.is_some());
551 let pubtime = metadata.pubtime.unwrap();
552 assert_eq!(pubtime.year(), 2025);
553 assert_eq!(pubtime.month(), 1);
554 assert_eq!(pubtime.day(), 2);
555 assert_eq!(pubtime.hour(), 9);
556 assert_eq!(pubtime.minute(), 5);
557 assert_eq!(pubtime.second(), 7);
558 }
559
560 #[test]
561 fn rust_version_round_trips_losslessly() {
562 let json = r#"{"name":"anyhow","vers":"1.0.100","deps":[],"cksum":"abc","features":{},"yanked":false,"v":2,"rust_version":"1.39"}"#;
565
566 let metadata: IndexMetadata = serde_json::from_str(json).unwrap();
567
568 assert_eq!(metadata.rust_version, Some("1.39".to_string()));
569 assert_eq!(
570 serde_json::from_str::<serde_json::Value>(&metadata.to_json().unwrap()).unwrap(),
571 serde_json::from_str::<serde_json::Value>(json).unwrap()
572 );
573 }
574
575 #[test]
576 fn rust_version_none_is_omitted_from_serialization() {
577 let metadata = IndexMetadata::minimal("test", "1.0.0", "abc123");
578
579 let json = metadata.to_json().unwrap();
580
581 assert!(
582 !json.contains("rust_version"),
583 "Expected rust_version to be omitted when None, got: {json}"
584 );
585 }
586
587 #[test]
588 fn serialize_indices_ends_with_newline_and_one_line_per_entry() {
589 let mk = |vers: &str| IndexMetadata {
590 name: "crate".to_string(),
591 vers: vers.to_string(),
592 deps: vec![],
593 cksum: "cksum".to_string(),
594 features: BTreeMap::default(),
595 yanked: false,
596 links: None,
597 pubtime: None,
598 v: Some(1),
599 features2: None,
600 rust_version: None,
601 };
602 let indices = vec![mk("1.0.0"), mk("2.0.0")];
603
604 let out = IndexMetadata::serialize_indices(&indices).unwrap();
605
606 assert!(
612 out.ends_with('\n'),
613 "sparse index body must end with a newline, got: {out:?}"
614 );
615
616 let lines: Vec<&str> = out.lines().collect();
618 assert_eq!(lines.len(), indices.len());
619 for (line, meta) in lines.iter().zip(&indices) {
620 let parsed: IndexMetadata = serde_json::from_str(line).unwrap();
621 assert_eq!(parsed.vers, meta.vers);
622 }
623 }
624}