1use std::borrow::Cow;
2use std::str::FromStr;
3
4use jiff::Timestamp;
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Deserializer, Serialize};
7
8use uv_normalize::{ExtraName, PackageName};
9use uv_pep440::{Version, VersionSpecifiers, VersionSpecifiersParseError};
10use uv_pep508::Requirement;
11use uv_small_str::SmallString;
12
13use crate::VerbatimParsedUrl;
14use crate::lenient_requirement::LenientVersionSpecifiers;
15
16#[derive(Debug, Clone, Deserialize)]
19pub struct PypiSimpleDetail {
20 #[serde(deserialize_with = "sorted_simple_json_files")]
22 pub files: Vec<PypiFile>,
23}
24
25fn sorted_simple_json_files<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<PypiFile>, D::Error> {
28 let mut files = <Vec<PypiFile>>::deserialize(d)?;
29 files.sort_unstable_by(|f1, f2| f1.filename.cmp(&f2.filename));
38 Ok(files)
39}
40
41#[derive(Debug, Clone)]
46pub struct PypiFile {
47 pub core_metadata: Option<CoreMetadata>,
48 pub filename: SmallString,
49 pub hashes: Hashes,
50 pub requires_python: Option<Result<VersionSpecifiers, VersionSpecifiersParseError>>,
51 pub size: Option<u64>,
52 pub upload_time: Option<Timestamp>,
53 pub url: SmallString,
54 pub yanked: Option<Box<Yanked>>,
55}
56
57impl<'de> Deserialize<'de> for PypiFile {
58 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59 where
60 D: Deserializer<'de>,
61 {
62 struct FileVisitor;
63
64 impl<'de> serde::de::Visitor<'de> for FileVisitor {
65 type Value = PypiFile;
66
67 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
68 formatter.write_str("a map containing file metadata")
69 }
70
71 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
72 where
73 M: serde::de::MapAccess<'de>,
74 {
75 let mut core_metadata = None;
76 let mut filename = None;
77 let mut hashes = None;
78 let mut requires_python = None;
79 let mut size = None;
80 let mut upload_time = None;
81 let mut url = None;
82 let mut yanked = None;
83
84 while let Some(key) = access.next_key::<String>()? {
85 match key.as_str() {
86 "core-metadata" | "dist-info-metadata" | "data-dist-info-metadata" => {
87 if core_metadata.is_none() {
88 core_metadata = access.next_value()?;
89 } else {
90 let _: serde::de::IgnoredAny = access.next_value()?;
91 }
92 }
93 "filename" => filename = Some(access.next_value()?),
94 "hashes" => hashes = Some(access.next_value()?),
95 "requires-python" => {
96 requires_python =
97 access.next_value::<Option<Cow<'_, str>>>()?.map(|s| {
98 LenientVersionSpecifiers::from_str(s.as_ref())
99 .map(VersionSpecifiers::from)
100 });
101 }
102 "size" => size = Some(access.next_value()?),
103 "upload-time" => upload_time = Some(access.next_value()?),
104 "url" => url = Some(access.next_value()?),
105 "yanked" => yanked = Some(access.next_value()?),
106 _ => {
107 let _: serde::de::IgnoredAny = access.next_value()?;
108 }
109 }
110 }
111
112 Ok(PypiFile {
113 core_metadata,
114 filename: filename
115 .ok_or_else(|| serde::de::Error::missing_field("filename"))?,
116 hashes: hashes.ok_or_else(|| serde::de::Error::missing_field("hashes"))?,
117 requires_python,
118 size,
119 upload_time,
120 url: url.ok_or_else(|| serde::de::Error::missing_field("url"))?,
121 yanked,
122 })
123 }
124 }
125
126 deserializer.deserialize_map(FileVisitor)
127 }
128}
129
130#[derive(Debug, Clone, Deserialize)]
132#[serde(rename_all = "kebab-case")]
133pub struct PyxSimpleDetail {
134 pub files: Vec<PyxFile>,
136 #[serde(default)]
138 pub core_metadata: FxHashMap<Version, CoreMetadatum>,
139}
140
141#[derive(Debug, Clone)]
144pub struct PyxFile {
145 pub core_metadata: Option<CoreMetadata>,
146 pub filename: Option<SmallString>,
147 pub hashes: Hashes,
148 pub requires_python: Option<Result<VersionSpecifiers, VersionSpecifiersParseError>>,
149 pub size: Option<u64>,
150 pub upload_time: Option<Timestamp>,
151 pub url: SmallString,
152 pub yanked: Option<Box<Yanked>>,
153 pub zstd: Option<Zstd>,
154}
155
156impl<'de> Deserialize<'de> for PyxFile {
157 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158 where
159 D: Deserializer<'de>,
160 {
161 struct FileVisitor;
162
163 impl<'de> serde::de::Visitor<'de> for FileVisitor {
164 type Value = PyxFile;
165
166 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
167 formatter.write_str("a map containing file metadata")
168 }
169
170 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
171 where
172 M: serde::de::MapAccess<'de>,
173 {
174 let mut core_metadata = None;
175 let mut filename = None;
176 let mut hashes = None;
177 let mut requires_python = None;
178 let mut size = None;
179 let mut upload_time = None;
180 let mut url = None;
181 let mut yanked = None;
182 let mut zstd = None;
183
184 while let Some(key) = access.next_key::<String>()? {
185 match key.as_str() {
186 "core-metadata" | "dist-info-metadata" | "data-dist-info-metadata" => {
187 if core_metadata.is_none() {
188 core_metadata = access.next_value()?;
189 } else {
190 let _: serde::de::IgnoredAny = access.next_value()?;
191 }
192 }
193 "filename" => filename = Some(access.next_value()?),
194 "hashes" => hashes = Some(access.next_value()?),
195 "requires-python" => {
196 requires_python =
197 access.next_value::<Option<Cow<'_, str>>>()?.map(|s| {
198 LenientVersionSpecifiers::from_str(s.as_ref())
199 .map(VersionSpecifiers::from)
200 });
201 }
202 "size" => size = Some(access.next_value()?),
203 "upload-time" => upload_time = Some(access.next_value()?),
204 "url" => url = Some(access.next_value()?),
205 "yanked" => yanked = Some(access.next_value()?),
206 "zstd" => {
207 zstd = Some(access.next_value()?);
208 }
209 _ => {
210 let _: serde::de::IgnoredAny = access.next_value()?;
211 }
212 }
213 }
214
215 Ok(PyxFile {
216 core_metadata,
217 filename,
218 hashes: hashes.ok_or_else(|| serde::de::Error::missing_field("hashes"))?,
219 requires_python,
220 size,
221 upload_time,
222 url: url.ok_or_else(|| serde::de::Error::missing_field("url"))?,
223 yanked,
224 zstd,
225 })
226 }
227 }
228
229 deserializer.deserialize_map(FileVisitor)
230 }
231}
232
233#[derive(Debug, Clone, Deserialize)]
234#[serde(rename_all = "kebab-case")]
235pub struct CoreMetadatum {
236 #[serde(default)]
237 pub requires_python: Option<VersionSpecifiers>,
238 #[serde(default)]
239 pub requires_dist: Box<[Requirement<VerbatimParsedUrl>]>,
240 #[serde(default, alias = "provides-extras")]
241 pub provides_extra: Box<[ExtraName]>,
242}
243
244#[derive(Debug, Clone)]
245pub enum CoreMetadata {
246 Bool(bool),
247 Hashes(Hashes),
248}
249
250impl<'de> Deserialize<'de> for CoreMetadata {
251 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252 where
253 D: Deserializer<'de>,
254 {
255 serde_untagged::UntaggedEnumVisitor::new()
256 .bool(|bool| Ok(Self::Bool(bool)))
257 .map(|map| map.deserialize().map(CoreMetadata::Hashes))
258 .deserialize(deserializer)
259 }
260}
261
262impl Serialize for CoreMetadata {
263 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
264 where
265 S: serde::Serializer,
266 {
267 match self {
268 Self::Bool(is_available) => serializer.serialize_bool(*is_available),
269 Self::Hashes(hashes) => hashes.serialize(serializer),
270 }
271 }
272}
273
274impl CoreMetadata {
275 pub fn is_available(&self) -> bool {
276 match self {
277 Self::Bool(is_available) => *is_available,
278 Self::Hashes(_) => true,
279 }
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
284#[rkyv(derive(Debug))]
285pub enum Yanked {
286 Bool(bool),
287 Reason(SmallString),
288}
289
290impl<'de> Deserialize<'de> for Yanked {
291 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
292 where
293 D: Deserializer<'de>,
294 {
295 serde_untagged::UntaggedEnumVisitor::new()
296 .bool(|bool| Ok(Self::Bool(bool)))
297 .string(|string| Ok(Self::Reason(SmallString::from(string))))
298 .deserialize(deserializer)
299 }
300}
301
302impl Serialize for Yanked {
303 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
304 where
305 S: serde::Serializer,
306 {
307 match self {
308 Self::Bool(is_yanked) => serializer.serialize_bool(*is_yanked),
309 Self::Reason(reason) => serializer.serialize_str(reason.as_ref()),
310 }
311 }
312}
313
314impl Yanked {
315 pub fn is_yanked(&self) -> bool {
316 match self {
317 Self::Bool(is_yanked) => *is_yanked,
318 Self::Reason(_) => true,
319 }
320 }
321}
322
323impl Default for Yanked {
324 fn default() -> Self {
325 Self::Bool(false)
326 }
327}
328
329#[derive(Debug, Clone, Eq, PartialEq, Default, Deserialize, Serialize)]
330pub struct Zstd {
331 pub hashes: Hashes,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub size: Option<u64>,
334}
335
336#[derive(Debug, Clone, Eq, PartialEq, Default, Deserialize, Serialize)]
340pub struct Hashes {
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub md5: Option<SmallString>,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub sha256: Option<SmallString>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub sha384: Option<SmallString>,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub sha512: Option<SmallString>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub blake2b: Option<SmallString>,
351}
352
353impl Hashes {
354 pub fn parse_fragment(fragment: &str) -> Result<Self, HashError> {
356 let mut parts = fragment.split('=');
357
358 let name = parts
360 .next()
361 .ok_or_else(|| HashError::InvalidFragment(fragment.to_string()))?;
362 let value = parts
363 .next()
364 .ok_or_else(|| HashError::InvalidFragment(fragment.to_string()))?;
365
366 if parts.next().is_some() {
368 return Err(HashError::InvalidFragment(fragment.to_string()));
369 }
370
371 match name {
372 "md5" => Ok(Self {
373 md5: Some(SmallString::from(value)),
374 sha256: None,
375 sha384: None,
376 sha512: None,
377 blake2b: None,
378 }),
379 "sha256" => Ok(Self {
380 md5: None,
381 sha256: Some(SmallString::from(value)),
382 sha384: None,
383 sha512: None,
384 blake2b: None,
385 }),
386 "sha384" => Ok(Self {
387 md5: None,
388 sha256: None,
389 sha384: Some(SmallString::from(value)),
390 sha512: None,
391 blake2b: None,
392 }),
393 "sha512" => Ok(Self {
394 md5: None,
395 sha256: None,
396 sha384: None,
397 sha512: Some(SmallString::from(value)),
398 blake2b: None,
399 }),
400 "blake2b" => Ok(Self {
401 md5: None,
402 sha256: None,
403 sha384: None,
404 sha512: None,
405 blake2b: Some(SmallString::from(value)),
406 }),
407 _ => Err(HashError::UnsupportedHashAlgorithm(fragment.to_string())),
408 }
409 }
410}
411
412impl FromStr for Hashes {
413 type Err = HashError;
414
415 fn from_str(s: &str) -> Result<Self, Self::Err> {
416 let mut parts = s.split(':');
417
418 let name = parts
420 .next()
421 .ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;
422 let value = parts
423 .next()
424 .ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;
425
426 if parts.next().is_some() {
428 return Err(HashError::InvalidStructure(s.to_string()));
429 }
430
431 match name {
432 "md5" => Ok(Self {
433 md5: Some(SmallString::from(value)),
434 sha256: None,
435 sha384: None,
436 sha512: None,
437 blake2b: None,
438 }),
439 "sha256" => Ok(Self {
440 md5: None,
441 sha256: Some(SmallString::from(value)),
442 sha384: None,
443 sha512: None,
444 blake2b: None,
445 }),
446 "sha384" => Ok(Self {
447 md5: None,
448 sha256: None,
449 sha384: Some(SmallString::from(value)),
450 sha512: None,
451 blake2b: None,
452 }),
453 "sha512" => Ok(Self {
454 md5: None,
455 sha256: None,
456 sha384: None,
457 sha512: Some(SmallString::from(value)),
458 blake2b: None,
459 }),
460 "blake2b" => Ok(Self {
461 md5: None,
462 sha256: None,
463 sha384: None,
464 sha512: None,
465 blake2b: Some(SmallString::from(value)),
466 }),
467 _ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())),
468 }
469 }
470}
471
472#[derive(
473 Debug,
474 Clone,
475 Copy,
476 Ord,
477 PartialOrd,
478 Eq,
479 PartialEq,
480 Hash,
481 Serialize,
482 Deserialize,
483 rkyv::Archive,
484 rkyv::Deserialize,
485 rkyv::Serialize,
486)]
487#[rkyv(derive(Debug))]
488pub enum HashAlgorithm {
489 Md5,
490 Sha256,
491 Sha384,
492 Sha512,
493 Blake2b,
494}
495
496impl FromStr for HashAlgorithm {
497 type Err = HashError;
498
499 fn from_str(s: &str) -> Result<Self, Self::Err> {
500 match s {
501 "md5" => Ok(Self::Md5),
502 "sha256" => Ok(Self::Sha256),
503 "sha384" => Ok(Self::Sha384),
504 "sha512" => Ok(Self::Sha512),
505 "blake2b" => Ok(Self::Blake2b),
506 _ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())),
507 }
508 }
509}
510
511impl std::fmt::Display for HashAlgorithm {
512 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513 match self {
514 Self::Md5 => write!(f, "md5"),
515 Self::Sha256 => write!(f, "sha256"),
516 Self::Sha384 => write!(f, "sha384"),
517 Self::Sha512 => write!(f, "sha512"),
518 Self::Blake2b => write!(f, "blake2b"),
519 }
520 }
521}
522
523#[derive(
525 Debug,
526 Clone,
527 Ord,
528 PartialOrd,
529 Eq,
530 PartialEq,
531 Hash,
532 Serialize,
533 Deserialize,
534 rkyv::Archive,
535 rkyv::Deserialize,
536 rkyv::Serialize,
537)]
538#[rkyv(derive(Debug))]
539pub struct HashDigest {
540 pub algorithm: HashAlgorithm,
541 pub digest: SmallString,
542}
543
544impl HashDigest {
545 pub fn algorithm(&self) -> HashAlgorithm {
547 self.algorithm
548 }
549}
550
551impl std::fmt::Display for HashDigest {
552 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553 write!(f, "{}:{}", self.algorithm, self.digest)
554 }
555}
556
557impl FromStr for HashDigest {
558 type Err = HashError;
559
560 fn from_str(s: &str) -> Result<Self, Self::Err> {
561 let mut parts = s.split(':');
562
563 let name = parts
565 .next()
566 .ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;
567 let value = parts
568 .next()
569 .ok_or_else(|| HashError::InvalidStructure(s.to_string()))?;
570
571 if parts.next().is_some() {
573 return Err(HashError::InvalidStructure(s.to_string()));
574 }
575
576 let algorithm = HashAlgorithm::from_str(name)?;
577 let digest = SmallString::from(value);
578
579 Ok(Self { algorithm, digest })
580 }
581}
582
583#[derive(
585 Debug,
586 Clone,
587 Ord,
588 PartialOrd,
589 Eq,
590 PartialEq,
591 Hash,
592 Serialize,
593 Deserialize,
594 rkyv::Archive,
595 rkyv::Deserialize,
596 rkyv::Serialize,
597)]
598#[rkyv(derive(Debug))]
599pub struct HashDigests(Box<[HashDigest]>);
600
601impl HashDigests {
602 pub fn empty() -> Self {
604 Self(Box::new([]))
605 }
606
607 pub fn as_slice(&self) -> &[HashDigest] {
609 self.0.as_ref()
610 }
611
612 pub fn is_empty(&self) -> bool {
614 self.0.is_empty()
615 }
616
617 pub fn first(&self) -> Option<&HashDigest> {
619 self.0.first()
620 }
621
622 pub fn to_vec(&self) -> Vec<HashDigest> {
624 self.0.to_vec()
625 }
626
627 pub fn iter(&self) -> impl Iterator<Item = &HashDigest> {
629 self.0.iter()
630 }
631
632 pub fn sort_unstable(&mut self) {
634 self.0.sort_unstable();
635 }
636}
637
638impl From<Hashes> for HashDigests {
640 fn from(value: Hashes) -> Self {
641 let mut digests = Vec::with_capacity(
642 usize::from(value.sha512.is_some())
643 + usize::from(value.sha384.is_some())
644 + usize::from(value.sha256.is_some())
645 + usize::from(value.md5.is_some()),
646 );
647 if let Some(sha512) = value.sha512 {
648 digests.push(HashDigest {
649 algorithm: HashAlgorithm::Sha512,
650 digest: sha512,
651 });
652 }
653 if let Some(sha384) = value.sha384 {
654 digests.push(HashDigest {
655 algorithm: HashAlgorithm::Sha384,
656 digest: sha384,
657 });
658 }
659 if let Some(sha256) = value.sha256 {
660 digests.push(HashDigest {
661 algorithm: HashAlgorithm::Sha256,
662 digest: sha256,
663 });
664 }
665 if let Some(md5) = value.md5 {
666 digests.push(HashDigest {
667 algorithm: HashAlgorithm::Md5,
668 digest: md5,
669 });
670 }
671 Self::from(digests)
672 }
673}
674
675impl From<HashDigests> for Hashes {
676 fn from(value: HashDigests) -> Self {
677 let mut hashes = Self::default();
678 for digest in value {
679 match digest.algorithm() {
680 HashAlgorithm::Md5 => hashes.md5 = Some(digest.digest),
681 HashAlgorithm::Sha256 => hashes.sha256 = Some(digest.digest),
682 HashAlgorithm::Sha384 => hashes.sha384 = Some(digest.digest),
683 HashAlgorithm::Sha512 => hashes.sha512 = Some(digest.digest),
684 HashAlgorithm::Blake2b => hashes.blake2b = Some(digest.digest),
685 }
686 }
687 hashes
688 }
689}
690
691impl From<HashDigest> for HashDigests {
692 fn from(value: HashDigest) -> Self {
693 Self(Box::new([value]))
694 }
695}
696
697impl From<&[HashDigest]> for HashDigests {
698 fn from(value: &[HashDigest]) -> Self {
699 Self(Box::from(value))
700 }
701}
702
703impl From<Vec<HashDigest>> for HashDigests {
704 fn from(value: Vec<HashDigest>) -> Self {
705 Self(value.into_boxed_slice())
706 }
707}
708
709impl FromIterator<HashDigest> for HashDigests {
710 fn from_iter<T: IntoIterator<Item = HashDigest>>(iter: T) -> Self {
711 Self(iter.into_iter().collect())
712 }
713}
714
715impl IntoIterator for HashDigests {
716 type Item = HashDigest;
717 type IntoIter = std::vec::IntoIter<HashDigest>;
718
719 fn into_iter(self) -> Self::IntoIter {
720 self.0.into_vec().into_iter()
721 }
722}
723
724#[derive(thiserror::Error, Debug)]
725pub enum HashError {
726 #[error("Unexpected hash (expected `<algorithm>:<hash>`): {0}")]
727 InvalidStructure(String),
728
729 #[error("Unexpected fragment (expected `#sha256=...` or similar) on URL: {0}")]
730 InvalidFragment(String),
731
732 #[error(
733 "Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, `sha512`, or `blake2b`) on: `{0}`"
734 )]
735 UnsupportedHashAlgorithm(String),
736}
737
738#[cfg(test)]
739mod tests {
740 use crate::{HashError, Hashes};
741
742 #[test]
743 fn parse_hashes() -> Result<(), HashError> {
744 let hashes: Hashes =
745 "blake2b:af4793213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a".parse()?;
746 assert_eq!(
747 hashes,
748 Hashes {
749 md5: None,
750 sha256: None,
751 sha384: None,
752 sha512: None,
753 blake2b: Some(
754 "af4793213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a".into()
755 ),
756 }
757 );
758
759 let hashes: Hashes =
760 "sha512:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?;
761 assert_eq!(
762 hashes,
763 Hashes {
764 md5: None,
765 sha256: None,
766 sha384: None,
767 sha512: Some(
768 "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into()
769 ),
770 blake2b: None,
771 }
772 );
773
774 let hashes: Hashes =
775 "sha384:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?;
776 assert_eq!(
777 hashes,
778 Hashes {
779 md5: None,
780 sha256: None,
781 sha384: Some(
782 "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into()
783 ),
784 sha512: None,
785 blake2b: None,
786 }
787 );
788
789 let hashes: Hashes =
790 "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?;
791 assert_eq!(
792 hashes,
793 Hashes {
794 md5: None,
795 sha256: Some(
796 "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into()
797 ),
798 sha384: None,
799 sha512: None,
800 blake2b: None,
801 }
802 );
803
804 let hashes: Hashes =
805 "md5:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".parse()?;
806 assert_eq!(
807 hashes,
808 Hashes {
809 md5: Some(
810 "090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".into()
811 ),
812 sha256: None,
813 sha384: None,
814 sha512: None,
815 blake2b: None,
816 }
817 );
818
819 let result = "sha256=40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"
820 .parse::<Hashes>();
821 assert!(result.is_err());
822
823 let result = "blake2:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"
824 .parse::<Hashes>();
825 assert!(result.is_err());
826
827 Ok(())
828 }
829}
830
831#[derive(Debug, Clone, Deserialize, Serialize)]
836#[serde(rename_all = "kebab-case")]
837pub struct PypiSimpleIndex {
838 pub meta: SimpleIndexMeta,
840 pub projects: Vec<ProjectEntry>,
842}
843
844#[derive(Debug, Clone, Deserialize, Serialize)]
847#[serde(rename_all = "kebab-case")]
848pub struct PyxSimpleIndex {
849 pub meta: SimpleIndexMeta,
851 pub projects: Vec<ProjectEntry>,
853}
854
855#[derive(Debug, Clone, Deserialize, Serialize)]
857#[serde(rename_all = "kebab-case")]
858pub struct SimpleIndexMeta {
859 pub api_version: SmallString,
861}
862
863#[derive(Debug, Clone, Deserialize, Serialize)]
865pub struct ProjectEntry {
866 pub name: PackageName,
868}