1use {
28 crate::{
29 control::{ControlParagraph, ControlParagraphReader},
30 error::{DebianError, Result},
31 io::ContentDigest,
32 repository::Compression,
33 },
34 chrono::{DateTime, Utc},
35 pgp_cleartext::CleartextHasher,
36 std::{
37 borrow::Cow,
38 io::BufRead,
39 ops::{Deref, DerefMut},
40 str::FromStr,
41 },
42};
43
44pub const DATE_FORMAT: &str = "%a, %d %b %Y %H:%M:%S %z";
46
47#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum ChecksumType {
50 Md5,
52
53 Sha1,
55
56 Sha256,
58}
59
60impl ChecksumType {
61 pub fn preferred_order() -> impl Iterator<Item = ChecksumType> {
63 [Self::Sha256, Self::Sha1, Self::Md5].into_iter()
64 }
65
66 pub fn field_name(&self) -> &'static str {
68 match self {
69 Self::Md5 => "MD5Sum",
70 Self::Sha1 => "SHA1",
71 Self::Sha256 => "SHA256",
72 }
73 }
74
75 pub fn new_hasher(&self) -> Box<dyn pgp::crypto::hash::Hasher + Send> {
77 Box::new(match self {
78 Self::Md5 => CleartextHasher::md5(),
79 Self::Sha1 => CleartextHasher::sha1(),
80 Self::Sha256 => CleartextHasher::sha256(),
81 })
82 }
83}
84
85#[derive(Clone, Debug, PartialEq, PartialOrd)]
95pub struct ReleaseFileEntry<'a> {
96 pub path: &'a str,
98
99 pub digest: ContentDigest,
101
102 pub size: u64,
104}
105
106impl<'a> ReleaseFileEntry<'a> {
107 pub fn by_hash_path(&self) -> String {
109 if let Some((prefix, _)) = self.path.rsplit_once('/') {
110 format!(
111 "{}/by-hash/{}/{}",
112 prefix,
113 self.digest.release_field_name(),
114 self.digest.digest_hex()
115 )
116 } else {
117 format!(
118 "by-hash/{}/{}",
119 self.digest.release_field_name(),
120 self.digest.digest_hex()
121 )
122 }
123 }
124}
125
126#[derive(Clone, Debug, PartialEq)]
130pub struct AppStreamComponentsEntry<'a> {
131 entry: ReleaseFileEntry<'a>,
133 pub component: Cow<'a, str>,
135 pub architecture: Cow<'a, str>,
137 pub compression: Compression,
139}
140
141impl<'a> Deref for AppStreamComponentsEntry<'a> {
142 type Target = ReleaseFileEntry<'a>;
143
144 fn deref(&self) -> &Self::Target {
145 &self.entry
146 }
147}
148
149impl<'a> DerefMut for AppStreamComponentsEntry<'a> {
150 fn deref_mut(&mut self) -> &mut Self::Target {
151 &mut self.entry
152 }
153}
154
155impl<'a> From<AppStreamComponentsEntry<'a>> for ReleaseFileEntry<'a> {
156 fn from(v: AppStreamComponentsEntry<'a>) -> Self {
157 v.entry
158 }
159}
160
161impl<'a> TryFrom<ReleaseFileEntry<'a>> for AppStreamComponentsEntry<'a> {
162 type Error = DebianError;
163
164 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
165 let parts = entry.path.split('/').collect::<Vec<_>>();
166
167 let filename = *parts
168 .last()
169 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
170
171 let suffix = filename
172 .strip_prefix("Components-")
173 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
174
175 let (architecture, remainder) = suffix
176 .split_once('.')
177 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
178
179 let compression = match remainder {
180 "yml" => Compression::None,
181 "yml.bz2" => Compression::Bzip2,
182 "yml.gz" => Compression::Gzip,
183 "yml.lzma" => Compression::Lzma,
184 "yml.xz" => Compression::Xz,
185 _ => {
186 return Err(DebianError::ReleaseIndicesEntryWrongType);
187 }
188 };
189
190 let component_end = entry
192 .path
193 .find("/dep11/Components-")
194 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
195 let component = &entry.path[0..component_end];
196
197 Ok(Self {
198 entry,
199 component: component.into(),
200 architecture: architecture.into(),
201 compression,
202 })
203 }
204}
205
206#[derive(Clone, Debug, PartialEq)]
210pub struct AppStreamIconsFileEntry<'a> {
211 entry: ReleaseFileEntry<'a>,
213 pub component: Cow<'a, str>,
215 pub resolution: Cow<'a, str>,
217 pub compression: Compression,
219}
220
221impl<'a> Deref for AppStreamIconsFileEntry<'a> {
222 type Target = ReleaseFileEntry<'a>;
223
224 fn deref(&self) -> &Self::Target {
225 &self.entry
226 }
227}
228
229impl<'a> DerefMut for AppStreamIconsFileEntry<'a> {
230 fn deref_mut(&mut self) -> &mut Self::Target {
231 &mut self.entry
232 }
233}
234
235impl<'a> From<AppStreamIconsFileEntry<'a>> for ReleaseFileEntry<'a> {
236 fn from(v: AppStreamIconsFileEntry<'a>) -> Self {
237 v.entry
238 }
239}
240
241impl<'a> TryFrom<ReleaseFileEntry<'a>> for AppStreamIconsFileEntry<'a> {
242 type Error = DebianError;
243
244 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
245 let parts = entry.path.split('/').collect::<Vec<_>>();
246
247 let filename = *parts
248 .last()
249 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
250
251 let suffix = filename
252 .strip_prefix("icons-")
253 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
254
255 let (resolution, remainder) = suffix
256 .split_once('.')
257 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
258
259 let compression = match remainder {
260 "tar" => Compression::None,
261 "tar.bz2" => Compression::Bzip2,
262 "tar.gz" => Compression::Gzip,
263 "tar.lzma" => Compression::Lzma,
264 "tar.xz" => Compression::Xz,
265 _ => {
266 return Err(DebianError::ReleaseIndicesEntryWrongType);
267 }
268 };
269
270 let component_end = entry
272 .path
273 .find("/dep11/icons-")
274 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
275 let component = &entry.path[0..component_end];
276
277 Ok(Self {
278 entry,
279 component: component.into(),
280 resolution: resolution.into(),
281 compression,
282 })
283 }
284}
285
286#[derive(Clone, Debug, PartialEq)]
290pub struct ContentsFileEntry<'a> {
291 entry: ReleaseFileEntry<'a>,
293
294 pub component: Option<Cow<'a, str>>,
296
297 pub architecture: Cow<'a, str>,
299
300 pub compression: Compression,
302
303 pub is_installer: bool,
305}
306
307impl<'a> Deref for ContentsFileEntry<'a> {
308 type Target = ReleaseFileEntry<'a>;
309
310 fn deref(&self) -> &Self::Target {
311 &self.entry
312 }
313}
314
315impl<'a> DerefMut for ContentsFileEntry<'a> {
316 fn deref_mut(&mut self) -> &mut Self::Target {
317 &mut self.entry
318 }
319}
320
321impl<'a> From<ContentsFileEntry<'a>> for ReleaseFileEntry<'a> {
322 fn from(v: ContentsFileEntry<'a>) -> Self {
323 v.entry
324 }
325}
326
327impl<'a> TryFrom<ReleaseFileEntry<'a>> for ContentsFileEntry<'a> {
328 type Error = DebianError;
329
330 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
331 let parts = entry.path.split('/').collect::<Vec<_>>();
332
333 let filename = *parts
334 .last()
335 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
336
337 let suffix = filename
338 .strip_prefix("Contents-")
339 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
340
341 let (architecture, compression) = if let Some(v) = suffix.strip_suffix(".gz") {
342 (v, Compression::Gzip)
343 } else {
344 (suffix, Compression::None)
345 };
346
347 let (architecture, is_installer) = if let Some(v) = architecture.strip_prefix("udeb-") {
348 (v, true)
349 } else {
350 (architecture, false)
351 };
352
353 let component = if parts.len() > 1 {
359 Some(Cow::from(
360 &entry.path[..entry.path.len() - filename.len() - 1],
361 ))
362 } else {
363 None
364 };
365
366 Ok(Self {
367 entry,
368 component,
369 architecture: architecture.into(),
370 compression,
371 is_installer,
372 })
373 }
374}
375
376#[derive(Clone, Debug, PartialEq)]
378pub struct PackagesFileEntry<'a> {
379 entry: ReleaseFileEntry<'a>,
381
382 pub component: Cow<'a, str>,
384
385 pub architecture: Cow<'a, str>,
387
388 pub compression: Compression,
390
391 pub is_installer: bool,
393}
394
395impl<'a> Deref for PackagesFileEntry<'a> {
396 type Target = ReleaseFileEntry<'a>;
397
398 fn deref(&self) -> &Self::Target {
399 &self.entry
400 }
401}
402
403impl<'a> DerefMut for PackagesFileEntry<'a> {
404 fn deref_mut(&mut self) -> &mut Self::Target {
405 &mut self.entry
406 }
407}
408
409impl<'a> From<PackagesFileEntry<'a>> for ReleaseFileEntry<'a> {
410 fn from(v: PackagesFileEntry<'a>) -> Self {
411 v.entry
412 }
413}
414
415impl<'a> TryFrom<ReleaseFileEntry<'a>> for PackagesFileEntry<'a> {
416 type Error = DebianError;
417
418 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
419 let parts = entry.path.split('/').collect::<Vec<_>>();
420
421 let compression = match *parts
422 .last()
423 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?
424 {
425 "Packages" => Compression::None,
426 "Packages.xz" => Compression::Xz,
427 "Packages.gz" => Compression::Gzip,
428 "Packages.bz2" => Compression::Bzip2,
429 "Packages.lzma" => Compression::Lzma,
430 _ => {
431 return Err(DebianError::ReleaseIndicesEntryWrongType);
432 }
433 };
434
435 let architecture_component = *parts
440 .iter()
441 .rev()
442 .nth(1)
443 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
444
445 let search = &entry.path[..entry.path.len()
446 - parts
447 .last()
448 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?
449 .len()
450 - 1];
451 let component = &search[0..search
452 .rfind('/')
453 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?];
454
455 let architecture = architecture_component
457 .strip_prefix("binary-")
458 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
459
460 let (component, is_udeb) =
462 if let Some(component) = component.strip_suffix("/debian-installer") {
463 (component, true)
464 } else {
465 (component, false)
466 };
467
468 Ok(Self {
469 entry,
470 component: component.into(),
471 architecture: architecture.into(),
472 compression,
473 is_installer: is_udeb,
474 })
475 }
476}
477
478#[derive(Clone, Debug, PartialEq)]
483pub struct ReleaseReleaseFileEntry<'a> {
484 entry: ReleaseFileEntry<'a>,
485}
486
487impl<'a> Deref for ReleaseReleaseFileEntry<'a> {
488 type Target = ReleaseFileEntry<'a>;
489
490 fn deref(&self) -> &Self::Target {
491 &self.entry
492 }
493}
494
495impl<'a> DerefMut for ReleaseReleaseFileEntry<'a> {
496 fn deref_mut(&mut self) -> &mut Self::Target {
497 &mut self.entry
498 }
499}
500
501impl<'a> From<ReleaseReleaseFileEntry<'a>> for ReleaseFileEntry<'a> {
502 fn from(v: ReleaseReleaseFileEntry<'a>) -> Self {
503 v.entry
504 }
505}
506
507impl<'a> TryFrom<ReleaseFileEntry<'a>> for ReleaseReleaseFileEntry<'a> {
508 type Error = DebianError;
509
510 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
511 let parts = entry.path.split('/').collect::<Vec<_>>();
512
513 if *parts
514 .last()
515 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?
516 != "Release"
517 {
518 return Err(DebianError::ReleaseIndicesEntryWrongType);
519 }
520
521 Ok(Self { entry })
522 }
523}
524
525#[derive(Clone, Debug, PartialEq)]
527pub struct SourcesFileEntry<'a> {
528 entry: ReleaseFileEntry<'a>,
529 pub component: Cow<'a, str>,
531 pub compression: Compression,
533}
534
535impl<'a> Deref for SourcesFileEntry<'a> {
536 type Target = ReleaseFileEntry<'a>;
537
538 fn deref(&self) -> &Self::Target {
539 &self.entry
540 }
541}
542
543impl<'a> DerefMut for SourcesFileEntry<'a> {
544 fn deref_mut(&mut self) -> &mut Self::Target {
545 &mut self.entry
546 }
547}
548
549impl<'a> From<SourcesFileEntry<'a>> for ReleaseFileEntry<'a> {
550 fn from(v: SourcesFileEntry<'a>) -> Self {
551 v.entry
552 }
553}
554
555impl<'a> TryFrom<ReleaseFileEntry<'a>> for SourcesFileEntry<'a> {
556 type Error = DebianError;
557
558 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
559 let parts = entry.path.split('/').collect::<Vec<_>>();
560
561 let compression = match *parts
562 .last()
563 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?
564 {
565 "Sources" => Compression::None,
566 "Sources.gz" => Compression::Gzip,
567 "Sources.xz" => Compression::Xz,
568 "Sources.bz2" => Compression::Bzip2,
569 "Sources.lzma" => Compression::Lzma,
570 _ => {
571 return Err(DebianError::ReleaseIndicesEntryWrongType);
572 }
573 };
574
575 let component = *parts
576 .first()
577 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
578
579 Ok(Self {
580 entry,
581 component: component.into(),
582 compression,
583 })
584 }
585}
586
587#[derive(Clone, Debug, PartialEq)]
591pub struct TranslationFileEntry<'a> {
592 entry: ReleaseFileEntry<'a>,
594
595 pub component: Cow<'a, str>,
597
598 pub locale: Cow<'a, str>,
600
601 pub compression: Compression,
603}
604
605impl<'a> Deref for TranslationFileEntry<'a> {
606 type Target = ReleaseFileEntry<'a>;
607
608 fn deref(&self) -> &Self::Target {
609 &self.entry
610 }
611}
612
613impl<'a> DerefMut for TranslationFileEntry<'a> {
614 fn deref_mut(&mut self) -> &mut Self::Target {
615 &mut self.entry
616 }
617}
618
619impl<'a> From<TranslationFileEntry<'a>> for ReleaseFileEntry<'a> {
620 fn from(v: TranslationFileEntry<'a>) -> Self {
621 v.entry
622 }
623}
624
625impl<'a> TryFrom<ReleaseFileEntry<'a>> for TranslationFileEntry<'a> {
626 type Error = DebianError;
627
628 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
629 let component_end = entry
631 .path
632 .find("/i18n/Translation-")
633 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
634 let component = &entry.path[0..component_end];
635
636 let parts = entry.path.split('/').collect::<Vec<_>>();
637
638 let filename = parts
639 .last()
640 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
641
642 let remainder = filename
643 .strip_prefix("Translation-")
644 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
645
646 let (locale, compression) = if let Some((locale, extension)) = remainder.split_once('.') {
647 let compression = match extension {
648 "gz" => Compression::Gzip,
649 "bz2" => Compression::Bzip2,
650 "lzma" => Compression::Lzma,
651 "xz" => Compression::Xz,
652 _ => {
653 return Err(DebianError::ReleaseIndicesEntryWrongType);
654 }
655 };
656
657 (locale, compression)
658 } else {
659 (remainder, Compression::None)
660 };
661
662 Ok(Self {
663 entry,
664 component: component.into(),
665 locale: locale.into(),
666 compression,
667 })
668 }
669}
670
671#[derive(Clone, Debug, PartialEq)]
676pub struct FileManifestEntry<'a> {
677 entry: ReleaseFileEntry<'a>,
679
680 pub checksum: ChecksumType,
682
683 pub root_path: Cow<'a, str>,
685}
686
687impl<'a> Deref for FileManifestEntry<'a> {
688 type Target = ReleaseFileEntry<'a>;
689
690 fn deref(&self) -> &Self::Target {
691 &self.entry
692 }
693}
694
695impl<'a> DerefMut for FileManifestEntry<'a> {
696 fn deref_mut(&mut self) -> &mut Self::Target {
697 &mut self.entry
698 }
699}
700
701impl<'a> From<FileManifestEntry<'a>> for ReleaseFileEntry<'a> {
702 fn from(v: FileManifestEntry<'a>) -> Self {
703 v.entry
704 }
705}
706
707impl<'a> TryFrom<ReleaseFileEntry<'a>> for FileManifestEntry<'a> {
708 type Error = DebianError;
709
710 fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
711 let parts = entry.path.split('/').collect::<Vec<_>>();
712
713 let filename = *parts
714 .last()
715 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
716
717 let checksum = match filename {
718 "MD5SUMS" => ChecksumType::Md5,
719 "SHA256SUMS" => ChecksumType::Sha256,
720 _ => {
721 return Err(DebianError::ReleaseIndicesEntryWrongType);
722 }
723 };
724
725 let root_path = entry
726 .path
727 .rsplit_once('/')
728 .ok_or(DebianError::ReleaseIndicesEntryWrongType)?
729 .0;
730
731 Ok(Self {
732 entry,
733 checksum,
734 root_path: root_path.into(),
735 })
736 }
737}
738
739#[derive(Debug)]
741pub enum ClassifiedReleaseFileEntry<'a> {
742 Contents(ContentsFileEntry<'a>),
744 Packages(PackagesFileEntry<'a>),
746 Sources(SourcesFileEntry<'a>),
748 Release(ReleaseReleaseFileEntry<'a>),
750 AppStreamComponents(AppStreamComponentsEntry<'a>),
752 AppStreamIcons(AppStreamIconsFileEntry<'a>),
754 Translation(TranslationFileEntry<'a>),
756 FileManifest(FileManifestEntry<'a>),
758 Other(ReleaseFileEntry<'a>),
760}
761
762impl<'a> Deref for ClassifiedReleaseFileEntry<'a> {
763 type Target = ReleaseFileEntry<'a>;
764
765 fn deref(&self) -> &Self::Target {
766 match self {
767 Self::Contents(v) => &v.entry,
768 Self::Packages(v) => &v.entry,
769 Self::Sources(v) => &v.entry,
770 Self::Release(v) => &v.entry,
771 Self::AppStreamComponents(v) => &v.entry,
772 Self::AppStreamIcons(v) => &v.entry,
773 Self::Translation(v) => &v.entry,
774 Self::FileManifest(v) => &v.entry,
775 Self::Other(v) => v,
776 }
777 }
778}
779
780impl<'a> DerefMut for ClassifiedReleaseFileEntry<'a> {
781 fn deref_mut(&mut self) -> &mut Self::Target {
782 match self {
783 Self::Contents(v) => &mut v.entry,
784 Self::Packages(v) => &mut v.entry,
785 Self::Sources(v) => &mut v.entry,
786 Self::Release(v) => &mut v.entry,
787 Self::AppStreamComponents(v) => &mut v.entry,
788 Self::AppStreamIcons(v) => &mut v.entry,
789 Self::Translation(v) => &mut v.entry,
790 Self::FileManifest(v) => &mut v.entry,
791 Self::Other(v) => v,
792 }
793 }
794}
795
796pub struct ReleaseFile<'a> {
807 paragraph: ControlParagraph<'a>,
808
809 signatures: Option<pgp_cleartext::CleartextSignatures>,
811}
812
813impl<'a> From<ControlParagraph<'a>> for ReleaseFile<'a> {
814 fn from(paragraph: ControlParagraph<'a>) -> Self {
815 Self {
816 paragraph,
817 signatures: None,
818 }
819 }
820}
821
822impl<'a> From<ReleaseFile<'a>> for ControlParagraph<'a> {
823 fn from(release: ReleaseFile<'a>) -> Self {
824 release.paragraph
825 }
826}
827
828impl<'a> Deref for ReleaseFile<'a> {
829 type Target = ControlParagraph<'a>;
830
831 fn deref(&self) -> &Self::Target {
832 &self.paragraph
833 }
834}
835
836impl<'a> DerefMut for ReleaseFile<'a> {
837 fn deref_mut(&mut self) -> &mut Self::Target {
838 &mut self.paragraph
839 }
840}
841
842impl<'a> ReleaseFile<'a> {
843 pub fn from_reader<R: BufRead>(reader: R) -> Result<Self> {
850 let paragraphs = ControlParagraphReader::new(reader).collect::<Result<Vec<_>>>()?;
851
852 if paragraphs.len() != 1 {
854 return Err(DebianError::ReleaseControlParagraphMismatch(
855 paragraphs.len(),
856 ));
857 }
858
859 let paragraph = paragraphs
860 .into_iter()
861 .next()
862 .expect("validated paragraph count above");
863
864 Ok(Self {
865 paragraph,
866 signatures: None,
867 })
868 }
869
870 pub fn from_armored_reader<R: BufRead>(reader: R) -> Result<Self> {
882 let reader = pgp_cleartext::CleartextSignatureReader::new(reader);
883 let mut reader = std::io::BufReader::new(reader);
884
885 let mut slf = Self::from_reader(&mut reader)?;
886 slf.signatures = Some(reader.into_inner().finalize());
887
888 Ok(slf)
889 }
890
891 pub fn signatures(&self) -> Option<&pgp_cleartext::CleartextSignatures> {
893 self.signatures.as_ref()
894 }
895
896 pub fn description(&self) -> Option<&str> {
898 self.field_str("Description")
899 }
900
901 pub fn origin(&self) -> Option<&str> {
903 self.field_str("Origin")
904 }
905
906 pub fn label(&self) -> Option<&str> {
908 self.field_str("Label")
909 }
910
911 pub fn version(&self) -> Option<&str> {
915 self.field_str("Version")
916 }
917
918 pub fn suite(&self) -> Option<&str> {
922 self.field_str("Suite")
923 }
924
925 pub fn codename(&self) -> Option<&str> {
927 self.field_str("Codename")
928 }
929
930 pub fn components(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
935 self.iter_field_words("Components")
936 }
937
938 pub fn architectures(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
942 self.iter_field_words("Architectures")
943 }
944
945 pub fn date_str(&self) -> Option<&str> {
947 self.field_str("Date")
948 }
949
950 pub fn date(&self) -> Option<Result<DateTime<Utc>>> {
954 self.field_datetime_rfc5322("Date")
955 }
956
957 pub fn valid_until_str(&self) -> Option<&str> {
959 self.field_str("Valid-Until")
960 }
961
962 pub fn valid_until(&self) -> Option<Result<DateTime<Utc>>> {
964 self.field_datetime_rfc5322("Valid-Until")
965 }
966
967 pub fn not_automatic(&self) -> Option<bool> {
971 self.field_bool("NotAutomatic")
972 }
973
974 pub fn but_automatic_upgrades(&self) -> Option<bool> {
978 self.field_bool("ButAutomaticUpgrades")
979 }
980
981 pub fn acquire_by_hash(&self) -> Option<bool> {
983 self.field_bool("Acquire-By-Hash")
984 }
985
986 pub fn iter_index_files(
995 &self,
996 checksum: ChecksumType,
997 ) -> Option<Box<(dyn Iterator<Item = Result<ReleaseFileEntry<'_>>> + '_)>> {
998 if let Some(iter) = self.iter_field_lines(checksum.field_name()) {
999 Some(Box::new(iter.map(move |v| {
1000 let mut parts = v.split_ascii_whitespace();
1003
1004 let digest = parts.next().ok_or(DebianError::ReleaseMissingDigest)?;
1005 let size = parts.next().ok_or(DebianError::ReleaseMissingSize)?;
1006 let path = parts.next().ok_or(DebianError::ReleaseMissingPath)?;
1007
1008 if parts.next().is_some() {
1010 return Err(DebianError::ReleasePathWithSpaces(v.to_string()));
1011 }
1012
1013 let digest = ContentDigest::from_hex_digest(checksum, digest)?;
1014 let size = u64::from_str(size)?;
1015
1016 Ok(ReleaseFileEntry { path, digest, size })
1017 })))
1018 } else {
1019 None
1020 }
1021 }
1022
1023 pub fn iter_classified_index_files(
1032 &self,
1033 checksum: ChecksumType,
1034 ) -> Option<Box<(dyn Iterator<Item = Result<ClassifiedReleaseFileEntry<'_>>> + '_)>> {
1035 if let Some(iter) = self.iter_index_files(checksum) {
1036 Some(Box::new(iter.map(|entry| match entry {
1037 Ok(entry) => {
1038 match ContentsFileEntry::try_from(entry.clone()) {
1042 Ok(contents) => {
1043 return Ok(ClassifiedReleaseFileEntry::Contents(contents));
1044 }
1045 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1046 Err(e) => {
1047 return Err(e);
1048 }
1049 }
1050
1051 match FileManifestEntry::try_from(entry.clone()) {
1052 Ok(entry) => {
1053 return Ok(ClassifiedReleaseFileEntry::FileManifest(entry));
1054 }
1055 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1056 Err(e) => {
1057 return Err(e);
1058 }
1059 }
1060
1061 match PackagesFileEntry::try_from(entry.clone()) {
1062 Ok(packages) => {
1063 return Ok(ClassifiedReleaseFileEntry::Packages(packages));
1064 }
1065 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1066 Err(e) => {
1067 return Err(e);
1068 }
1069 }
1070
1071 match ReleaseReleaseFileEntry::try_from(entry.clone()) {
1072 Ok(release) => {
1073 return Ok(ClassifiedReleaseFileEntry::Release(release));
1074 }
1075 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1076 Err(e) => {
1077 return Err(e);
1078 }
1079 }
1080
1081 match AppStreamComponentsEntry::try_from(entry.clone()) {
1082 Ok(components) => {
1083 return Ok(ClassifiedReleaseFileEntry::AppStreamComponents(components));
1084 }
1085 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1086 Err(e) => {
1087 return Err(e);
1088 }
1089 }
1090
1091 match AppStreamIconsFileEntry::try_from(entry.clone()) {
1092 Ok(icons) => {
1093 return Ok(ClassifiedReleaseFileEntry::AppStreamIcons(icons));
1094 }
1095 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1096 Err(e) => {
1097 return Err(e);
1098 }
1099 }
1100
1101 match TranslationFileEntry::try_from(entry.clone()) {
1102 Ok(entry) => {
1103 return Ok(ClassifiedReleaseFileEntry::Translation(entry));
1104 }
1105 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1106 Err(e) => {
1107 return Err(e);
1108 }
1109 }
1110
1111 match SourcesFileEntry::try_from(entry.clone()) {
1112 Ok(sources) => {
1113 return Ok(ClassifiedReleaseFileEntry::Sources(sources));
1114 }
1115 Err(DebianError::ReleaseIndicesEntryWrongType) => {}
1116 Err(e) => {
1117 return Err(e);
1118 }
1119 }
1120
1121 Ok(ClassifiedReleaseFileEntry::Other(entry))
1122 }
1123 Err(e) => Err(e),
1124 })))
1125 } else {
1126 None
1127 }
1128 }
1129
1130 pub fn iter_contents_indices(
1138 &self,
1139 checksum: ChecksumType,
1140 ) -> Option<Box<(dyn Iterator<Item = Result<ContentsFileEntry<'_>>> + '_)>> {
1141 if let Some(iter) = self.iter_index_files(checksum) {
1142 Some(Box::new(iter.filter_map(|entry| match entry {
1143 Ok(entry) => match ContentsFileEntry::try_from(entry) {
1144 Ok(v) => Some(Ok(v)),
1145 Err(DebianError::ReleaseIndicesEntryWrongType) => None,
1146 Err(e) => Some(Err(e)),
1147 },
1148 Err(e) => Some(Err(e)),
1149 })))
1150 } else {
1151 None
1152 }
1153 }
1154
1155 pub fn iter_packages_indices(
1163 &self,
1164 checksum: ChecksumType,
1165 ) -> Option<Box<(dyn Iterator<Item = Result<PackagesFileEntry<'_>>> + '_)>> {
1166 if let Some(iter) = self.iter_index_files(checksum) {
1167 Some(Box::new(iter.filter_map(|entry| match entry {
1168 Ok(entry) => match PackagesFileEntry::try_from(entry) {
1169 Ok(v) => Some(Ok(v)),
1170 Err(DebianError::ReleaseIndicesEntryWrongType) => None,
1171 Err(e) => Some(Err(e)),
1172 },
1173 Err(e) => Some(Err(e)),
1174 })))
1175 } else {
1176 None
1177 }
1178 }
1179
1180 pub fn find_packages_indices(
1182 &self,
1183 checksum: ChecksumType,
1184 compression: Compression,
1185 component: &str,
1186 arch: &str,
1187 is_installer: bool,
1188 ) -> Option<PackagesFileEntry<'_>> {
1189 if let Some(mut iter) = self.iter_packages_indices(checksum) {
1190 iter.find_map(|entry| {
1191 if let Ok(entry) = entry {
1192 if entry.component == component
1193 && entry.architecture == arch
1194 && entry.is_installer == is_installer
1195 && entry.compression == compression
1196 {
1197 Some(entry)
1198 } else {
1199 None
1200 }
1201 } else {
1202 None
1203 }
1204 })
1205 } else {
1206 None
1207 }
1208 }
1209
1210 pub fn iter_sources_indices(
1214 &self,
1215 checksum: ChecksumType,
1216 ) -> Option<Box<(dyn Iterator<Item = Result<SourcesFileEntry<'_>>> + '_)>> {
1217 if let Some(iter) = self.iter_index_files(checksum) {
1218 Some(Box::new(iter.filter_map(|entry| match entry {
1219 Ok(entry) => match SourcesFileEntry::try_from(entry) {
1220 Ok(v) => Some(Ok(v)),
1221 Err(DebianError::ReleaseIndicesEntryWrongType) => None,
1222 Err(e) => Some(Err(e)),
1223 },
1224 Err(e) => Some(Err(e)),
1225 })))
1226 } else {
1227 None
1228 }
1229 }
1230
1231 pub fn find_sources_indices(
1233 &self,
1234 checksum: ChecksumType,
1235 compression: Compression,
1236 component: &str,
1237 ) -> Option<SourcesFileEntry<'_>> {
1238 if let Some(mut iter) = self.iter_sources_indices(checksum) {
1239 iter.find_map(|entry| {
1240 if let Ok(entry) = entry {
1241 if entry.component == component && entry.compression == compression {
1242 Some(entry)
1243 } else {
1244 None
1245 }
1246 } else {
1247 None
1248 }
1249 })
1250 } else {
1251 None
1252 }
1253 }
1254}
1255
1256#[cfg(test)]
1257mod test {
1258 use super::*;
1259
1260 #[test]
1261 fn parse_bullseye_release() -> Result<()> {
1262 let mut reader =
1263 std::io::Cursor::new(include_bytes!("../testdata/release-debian-bullseye"));
1264
1265 let release = ReleaseFile::from_reader(&mut reader)?;
1266
1267 assert_eq!(
1268 release.description(),
1269 Some("Debian 11.1 Released 09 October 2021")
1270 );
1271 assert_eq!(release.origin(), Some("Debian"));
1272 assert_eq!(release.label(), Some("Debian"));
1273 assert_eq!(release.version(), Some("11.1"));
1274 assert_eq!(release.suite(), Some("stable"));
1275 assert_eq!(release.codename(), Some("bullseye"));
1276 assert_eq!(
1277 release.components().unwrap().collect::<Vec<_>>(),
1278 vec!["main", "contrib", "non-free"]
1279 );
1280 assert_eq!(
1281 release.architectures().unwrap().collect::<Vec<_>>(),
1282 vec![
1283 "all", "amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el",
1284 "s390x"
1285 ]
1286 );
1287 assert_eq!(release.date_str(), Some("Sat, 09 Oct 2021 09:34:56 UTC"));
1288 assert_eq!(
1289 release.date().unwrap()?,
1290 DateTime::<Utc>::from_naive_utc_and_offset(
1291 chrono::NaiveDateTime::new(
1292 chrono::NaiveDate::from_ymd_opt(2021, 10, 9).unwrap(),
1293 chrono::NaiveTime::from_hms_opt(9, 34, 56).unwrap()
1294 ),
1295 Utc
1296 )
1297 );
1298
1299 assert!(release.valid_until_str().is_none());
1300
1301 let entries = release
1302 .iter_index_files(ChecksumType::Md5)
1303 .unwrap()
1304 .collect::<Result<Vec<_>>>()?;
1305 assert_eq!(entries.len(), 600);
1306 assert_eq!(
1307 entries[0],
1308 ReleaseFileEntry {
1309 path: "contrib/Contents-all",
1310 digest: ContentDigest::md5_hex("7fdf4db15250af5368cc52a91e8edbce").unwrap(),
1311 size: 738242,
1312 }
1313 );
1314 assert_eq!(
1315 entries[0].by_hash_path(),
1316 "contrib/by-hash/MD5Sum/7fdf4db15250af5368cc52a91e8edbce"
1317 );
1318 assert_eq!(
1319 entries[1],
1320 ReleaseFileEntry {
1321 path: "contrib/Contents-all.gz",
1322 digest: ContentDigest::md5_hex("cbd7bc4d3eb517ac2b22f929dfc07b47").unwrap(),
1323 size: 57319,
1324 }
1325 );
1326 assert_eq!(
1327 entries[1].by_hash_path(),
1328 "contrib/by-hash/MD5Sum/cbd7bc4d3eb517ac2b22f929dfc07b47"
1329 );
1330 assert_eq!(
1331 entries[599],
1332 ReleaseFileEntry {
1333 path: "non-free/source/Sources.xz",
1334 digest: ContentDigest::md5_hex("e3830f6fc5a946b5a5b46e8277e1d86f").unwrap(),
1335 size: 80488,
1336 }
1337 );
1338 assert_eq!(
1339 entries[599].by_hash_path(),
1340 "non-free/source/by-hash/MD5Sum/e3830f6fc5a946b5a5b46e8277e1d86f"
1341 );
1342
1343 assert!(release.iter_index_files(ChecksumType::Sha1).is_none());
1344
1345 let entries = release
1346 .iter_index_files(ChecksumType::Sha256)
1347 .unwrap()
1348 .collect::<Result<Vec<_>>>()?;
1349 assert_eq!(entries.len(), 600);
1350 assert_eq!(
1351 entries[0],
1352 ReleaseFileEntry {
1353 path: "contrib/Contents-all",
1354 digest: ContentDigest::sha256_hex(
1355 "3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63"
1356 )
1357 .unwrap(),
1358 size: 738242,
1359 }
1360 );
1361 assert_eq!(entries[0].by_hash_path(), "contrib/by-hash/SHA256/3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63");
1362 assert_eq!(
1363 entries[1],
1364 ReleaseFileEntry {
1365 path: "contrib/Contents-all.gz",
1366 digest: ContentDigest::sha256_hex(
1367 "3e9a121d599b56c08bc8f144e4830807c77c29d7114316d6984ba54695d3db7b"
1368 )
1369 .unwrap(),
1370 size: 57319,
1371 }
1372 );
1373 assert_eq!(entries[1].by_hash_path(), "contrib/by-hash/SHA256/3e9a121d599b56c08bc8f144e4830807c77c29d7114316d6984ba54695d3db7b");
1374 assert_eq!(
1375 entries[599],
1376 ReleaseFileEntry {
1377 digest: ContentDigest::sha256_hex(
1378 "30f3f996941badb983141e3b29b2ed5941d28cf81f9b5f600bb48f782d386fc7"
1379 )
1380 .unwrap(),
1381 size: 80488,
1382 path: "non-free/source/Sources.xz",
1383 }
1384 );
1385 assert_eq!(entries[599].by_hash_path(), "non-free/source/by-hash/SHA256/30f3f996941badb983141e3b29b2ed5941d28cf81f9b5f600bb48f782d386fc7");
1386
1387 const EXPECTED_CONTENTS: usize = 126;
1388 const EXPECTED_PACKAGES: usize = 180;
1389 const EXPECTED_SOURCES: usize = 9;
1390 const EXPECTED_RELEASE: usize = 63;
1391 const EXPECTED_APPSTREAM_COMPONENTS: usize = 72;
1392 const EXPECTED_APPSTREAM_ICONS: usize = 18;
1393 const EXPECTED_TRANSLATION: usize = 78;
1394 const EXPECTED_FILEMANIFEST: usize = 54;
1395 const EXPECTED_OTHER: usize = 600
1396 - EXPECTED_CONTENTS
1397 - EXPECTED_PACKAGES
1398 - EXPECTED_SOURCES
1399 - EXPECTED_RELEASE
1400 - EXPECTED_APPSTREAM_COMPONENTS
1401 - EXPECTED_APPSTREAM_ICONS
1402 - EXPECTED_TRANSLATION
1403 - EXPECTED_FILEMANIFEST;
1404
1405 assert_eq!(EXPECTED_OTHER, 0);
1406
1407 let entries = release
1408 .iter_classified_index_files(ChecksumType::Sha256)
1409 .unwrap()
1410 .collect::<Result<Vec<_>>>()?;
1411 assert_eq!(entries.len(), 600);
1412 assert_eq!(
1413 entries
1414 .iter()
1415 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Contents(_)))
1416 .count(),
1417 EXPECTED_CONTENTS
1418 );
1419 assert_eq!(
1420 entries
1421 .iter()
1422 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Packages(_)))
1423 .count(),
1424 EXPECTED_PACKAGES
1425 );
1426 assert_eq!(
1427 entries
1428 .iter()
1429 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Sources(_)))
1430 .count(),
1431 EXPECTED_SOURCES
1432 );
1433 assert_eq!(
1434 entries
1435 .iter()
1436 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Release(_)))
1437 .count(),
1438 EXPECTED_RELEASE
1439 );
1440 assert_eq!(
1441 entries
1442 .iter()
1443 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::AppStreamComponents(_)))
1444 .count(),
1445 EXPECTED_APPSTREAM_COMPONENTS
1446 );
1447 assert_eq!(
1448 entries
1449 .iter()
1450 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::AppStreamIcons(_)))
1451 .count(),
1452 EXPECTED_APPSTREAM_ICONS
1453 );
1454 assert_eq!(
1455 entries
1456 .iter()
1457 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Translation(_)))
1458 .count(),
1459 EXPECTED_TRANSLATION
1460 );
1461 assert_eq!(
1462 entries
1463 .iter()
1464 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::FileManifest(_)))
1465 .count(),
1466 EXPECTED_FILEMANIFEST
1467 );
1468 assert_eq!(
1469 entries
1470 .iter()
1471 .filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Other(_)))
1472 .count(),
1473 EXPECTED_OTHER
1474 );
1475
1476 let contents = release
1477 .iter_contents_indices(ChecksumType::Sha256)
1478 .unwrap()
1479 .collect::<Result<Vec<_>>>()?;
1480 assert_eq!(contents.len(), EXPECTED_CONTENTS);
1481
1482 assert_eq!(
1483 contents[0],
1484 ContentsFileEntry {
1485 entry: ReleaseFileEntry {
1486 path: "contrib/Contents-all",
1487 digest: ContentDigest::sha256_hex(
1488 "3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63"
1489 )
1490 .unwrap(),
1491 size: 738242,
1492 },
1493 component: Some("contrib".into()),
1494 architecture: "all".into(),
1495 compression: Compression::None,
1496 is_installer: false
1497 }
1498 );
1499 assert_eq!(
1500 contents[1],
1501 ContentsFileEntry {
1502 entry: ReleaseFileEntry {
1503 path: "contrib/Contents-all.gz",
1504 digest: ContentDigest::sha256_hex(
1505 "3e9a121d599b56c08bc8f144e4830807c77c29d7114316d6984ba54695d3db7b"
1506 )
1507 .unwrap(),
1508 size: 57319,
1509 },
1510 component: Some("contrib".into()),
1511 architecture: "all".into(),
1512 compression: Compression::Gzip,
1513 is_installer: false
1514 }
1515 );
1516 assert_eq!(
1517 contents[24],
1518 ContentsFileEntry {
1519 entry: ReleaseFileEntry {
1520 path: "contrib/Contents-udeb-amd64",
1521 digest: ContentDigest::sha256_hex(
1522 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
1523 )
1524 .unwrap(),
1525 size: 0,
1526 },
1527 component: Some("contrib".into()),
1528 architecture: "amd64".into(),
1529 compression: Compression::None,
1530 is_installer: true
1531 }
1532 );
1533
1534 let packages = release
1535 .iter_packages_indices(ChecksumType::Sha256)
1536 .unwrap()
1537 .collect::<Result<Vec<_>>>()?;
1538 assert_eq!(packages.len(), EXPECTED_PACKAGES);
1539
1540 assert_eq!(
1541 packages[0],
1542 PackagesFileEntry {
1543 entry: ReleaseFileEntry {
1544 path: "contrib/binary-all/Packages",
1545 digest: ContentDigest::sha256_hex(
1546 "48cfe101cd84f16baf720b99e8f2ff89fd7e063553966d8536b472677acb82f0"
1547 )
1548 .unwrap(),
1549 size: 103223,
1550 },
1551 component: "contrib".into(),
1552 architecture: "all".into(),
1553 compression: Compression::None,
1554 is_installer: false
1555 }
1556 );
1557 assert_eq!(
1558 packages[1],
1559 PackagesFileEntry {
1560 entry: ReleaseFileEntry {
1561 path: "contrib/binary-all/Packages.gz",
1562 digest: ContentDigest::sha256_hex(
1563 "86057fcd3eff667ec8e3fbabb2a75e229f5e99f39ace67ff0db4a8509d0707e4"
1564 )
1565 .unwrap(),
1566 size: 27334,
1567 },
1568 component: "contrib".into(),
1569 architecture: "all".into(),
1570 compression: Compression::Gzip,
1571 is_installer: false
1572 }
1573 );
1574 assert_eq!(
1575 packages[2],
1576 PackagesFileEntry {
1577 entry: ReleaseFileEntry {
1578 path: "contrib/binary-all/Packages.xz",
1579 digest: ContentDigest::sha256_hex(
1580 "706c840235798e098d4d6013d1dabbc967f894d0ffa02c92ac959dcea85ddf54"
1581 )
1582 .unwrap(),
1583 size: 23912,
1584 },
1585 component: "contrib".into(),
1586 architecture: "all".into(),
1587 compression: Compression::Xz,
1588 is_installer: false
1589 }
1590 );
1591
1592 let udeps = packages
1593 .into_iter()
1594 .filter(|x| x.is_installer)
1595 .collect::<Vec<_>>();
1596
1597 assert_eq!(udeps.len(), 90);
1598 assert_eq!(
1599 udeps[0],
1600 PackagesFileEntry {
1601 entry: ReleaseFileEntry {
1602 path: "contrib/debian-installer/binary-all/Packages",
1603 digest: ContentDigest::sha256_hex(
1604 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
1605 )
1606 .unwrap(),
1607 size: 0,
1608 },
1609 component: "contrib".into(),
1610 architecture: "all".into(),
1611 compression: Compression::None,
1612 is_installer: true
1613 }
1614 );
1615
1616 let sources = release
1617 .iter_sources_indices(ChecksumType::Sha256)
1618 .unwrap()
1619 .collect::<Result<Vec<_>>>()?;
1620 assert_eq!(sources.len(), EXPECTED_SOURCES);
1621
1622 let entry = release
1623 .find_sources_indices(ChecksumType::Sha256, Compression::Xz, "main")
1624 .unwrap();
1625 assert_eq!(
1626 entry,
1627 SourcesFileEntry {
1628 entry: ReleaseFileEntry {
1629 path: "main/source/Sources.xz",
1630 digest: ContentDigest::sha256_hex(
1631 "1801d18c1135168d5dd86a8cb85fb5cd5bd81e16174acc25d900dee11389e9cd"
1632 )
1633 .unwrap(),
1634 size: 8616784,
1635 },
1636 component: "main".into(),
1637 compression: Compression::Xz
1638 }
1639 );
1640
1641 Ok(())
1642 }
1643
1644 fn bullseye_signing_key() -> pgp::SignedPublicKey {
1645 crate::signing_key::DistroSigningKey::Debian11Release.public_key()
1646 }
1647
1648 #[test]
1649 fn parse_bullseye_inrelease() -> Result<()> {
1650 let reader = std::io::Cursor::new(include_bytes!("../testdata/inrelease-debian-bullseye"));
1651
1652 let release = ReleaseFile::from_armored_reader(reader)?;
1653
1654 let signing_key = bullseye_signing_key();
1655
1656 assert_eq!(release.signatures.unwrap().verify(&signing_key).unwrap(), 1);
1657
1658 Ok(())
1659 }
1660
1661 #[test]
1662 fn bad_signature_rejection() -> Result<()> {
1663 let reader = std::io::Cursor::new(
1664 include_str!("../testdata/inrelease-debian-bullseye").replace(
1665 "d41d8cd98f00b204e9800998ecf8427e",
1666 "d41d8cd98f00b204e9800998ecf80000",
1667 ),
1668 );
1669 let release = ReleaseFile::from_armored_reader(reader)?;
1670
1671 let signing_key = bullseye_signing_key();
1672
1673 assert!(release.signatures.unwrap().verify(&signing_key).is_err());
1674
1675 Ok(())
1676 }
1677
1678 #[test]
1679 fn focal_release() -> Result<()> {
1680 let mut reader = std::io::Cursor::new(include_bytes!("../testdata/release-ubuntu-focal"));
1681
1682 let release = ReleaseFile::from_reader(&mut reader)?;
1683
1684 let contents = release
1685 .iter_contents_indices(ChecksumType::Sha256)
1686 .unwrap()
1687 .collect::<Result<Vec<_>>>()?;
1688
1689 assert_eq!(contents.len(), 14);
1690
1691 assert_eq!(
1692 contents[0],
1693 ContentsFileEntry {
1694 entry: ReleaseFileEntry {
1695 path: "Contents-riscv64",
1696 digest: ContentDigest::sha256_hex(
1697 "0f27d95c6df5c174622e8c42e2b2f1cad636af6296ae981e87a2a7d4cdd572db"
1698 )
1699 .unwrap(),
1700 size: 603064135,
1701 },
1702 component: None,
1703 architecture: "riscv64".into(),
1704 compression: Compression::None,
1705 is_installer: false,
1706 }
1707 );
1708
1709 Ok(())
1710 }
1711}