debian_packaging/repository/
release.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! `Release` file primitives.
6
7`Release` files (or `InRelease` if it contains a PGP cleartext signature) are
8the main definition of a Debian repository. They are a control paragraph that
9defines repository-level metadata as well as a list of additional *indices* files
10that further define the content of the repository.
11
12[ReleaseFile] represents a parsed `Release` or `InRelease` file. It exposes
13accessor functions for obtaining well-known metadata fields. It also exposes
14various functions for obtaining index file entries.
15
16[ReleaseFileEntry] is the most generic type describing an *indices* file.
17Additional types describe more strongly typed indices file variants:
18
19* [ContentsFileEntry] (`Contents` files)
20* [PackagesFileEntry] (`Packages` files)
21* [SourcesFileEntry] (`Sources` files)
22
23The [ClassifiedReleaseFileEntry] enum wraps all these types and attempts to
24classify each entry as the strongest type possible.
25*/
26
27use {
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
44/// Formatter string for dates in release files.
45pub const DATE_FORMAT: &str = "%a, %d %b %Y %H:%M:%S %z";
46
47/// Checksum type / digest mechanism used in a release file.
48#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum ChecksumType {
50    /// MD5.
51    Md5,
52
53    /// SHA-1.
54    Sha1,
55
56    /// SHA-256.
57    Sha256,
58}
59
60impl ChecksumType {
61    /// Emit variants in their preferred usage order.
62    pub fn preferred_order() -> impl Iterator<Item = ChecksumType> {
63        [Self::Sha256, Self::Sha1, Self::Md5].into_iter()
64    }
65
66    /// Name of the control field in `Release` files holding this variant type.
67    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    /// Obtain a new hasher for this checksum flavor.
76    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/// An entry for a file in a parsed `Release` file.
86///
87/// Instances correspond to a line in a `MD5Sum`, `SHA1`, or `SHA256` field.
88///
89/// This is the most generic way to represent an indices file in a `Release` file.
90///
91/// Instances can be fallibly converted into more strongly typed release entries
92/// via [TryFrom]/[TryInto]. Other entry types include [ContentsFileEntry],
93/// [PackagesFileEntry], and [SourcesFileEntry].
94#[derive(Clone, Debug, PartialEq, PartialOrd)]
95pub struct ReleaseFileEntry<'a> {
96    /// The path to this file within the repository.
97    pub path: &'a str,
98
99    /// The content digest of this file.
100    pub digest: ContentDigest,
101
102    /// The size of the file in bytes.
103    pub size: u64,
104}
105
106impl<'a> ReleaseFileEntry<'a> {
107    /// Obtain the `by-hash` path variant for this entry.
108    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/// A type of [ReleaseFileEntry] that describes an AppStream `Components` YAML file.
127///
128/// Files typically exist in paths named `<component>/dep11/Components-<architecture><compression>`.
129#[derive(Clone, Debug, PartialEq)]
130pub struct AppStreamComponentsEntry<'a> {
131    /// The [ReleaseFileEntry] from which this instance was derived.
132    entry: ReleaseFileEntry<'a>,
133    /// The repository component name.
134    pub component: Cow<'a, str>,
135    /// The architecture name.
136    pub architecture: Cow<'a, str>,
137    /// File-level compression format.
138    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        // The component is the part up until the `/dep11/Components-` pattern.
191        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/// A type of [ReleaseFileEntry] that describes an AppStream `icons` archive.
207///
208/// Files typically exist in paths named `<component>/dep11/icons-<size><compression>`.
209#[derive(Clone, Debug, PartialEq)]
210pub struct AppStreamIconsFileEntry<'a> {
211    /// The [ReleaseFileEntry] from which this instance was derived.
212    entry: ReleaseFileEntry<'a>,
213    /// The repository component name.
214    pub component: Cow<'a, str>,
215    /// The pixel resolution of the icons. e.g. `128x128`.
216    pub resolution: Cow<'a, str>,
217    /// File-level compression format.s
218    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        // The component is the part up until the `/dep11/icons-` pattern.
271        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/// A type of [ReleaseFileEntry] that describes a `Contents` file.
287///
288/// This represents a pre-parsed wrapper around a [ReleaseFileEntry].
289#[derive(Clone, Debug, PartialEq)]
290pub struct ContentsFileEntry<'a> {
291    /// The [ReleaseFileEntry] from which this instance was derived.
292    entry: ReleaseFileEntry<'a>,
293
294    /// The parsed component name (from the entry's path).
295    pub component: Option<Cow<'a, str>>,
296
297    /// The parsed architecture name (from the entry's path).
298    pub architecture: Cow<'a, str>,
299
300    /// File-level compression format being used.
301    pub compression: Compression,
302
303    /// Whether this refers to udeb packages used by installers.
304    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        // Release files can annotate Contents files at the root directory or
354        // in component sub-directories. If a subdirectory is present, the part
355        // up to the Contents filename is the component. Otherwise it is a global
356        // Contents file with no component annotated.
357
358        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/// A special type of [ReleaseFileEntry] that describes a `Packages` file.
377#[derive(Clone, Debug, PartialEq)]
378pub struct PackagesFileEntry<'a> {
379    /// The [ReleaseFileEntry] from which this instance was derived.
380    entry: ReleaseFileEntry<'a>,
381
382    /// The parsed component name (from the entry's path).
383    pub component: Cow<'a, str>,
384
385    /// The parsed architecture name (from the entry's path).
386    pub architecture: Cow<'a, str>,
387
388    /// File-level compression format being used.
389    pub compression: Compression,
390
391    /// Whether this refers to udeb packages used by installers.
392    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        // The component and architecture are the directory components before the
436        // filename. The architecture is limited to a single directory component but
437        // the component can have multiple directories.
438
439        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        // The architecture part is prefixed with `binary-`.
456        let architecture = architecture_component
457            .strip_prefix("binary-")
458            .ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
459
460        // udeps have a `debian-installer` path component following the component.
461        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/// A type of [ReleaseFileEntry] that describes a nested `Release` file.
479///
480/// These often appear next to `Packages` or `Sources` files and contain a control paragraph
481/// to describe the defined component.
482#[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/// A type of [ReleaseFileEntry] that describes a `Sources` file.
526#[derive(Clone, Debug, PartialEq)]
527pub struct SourcesFileEntry<'a> {
528    entry: ReleaseFileEntry<'a>,
529    /// The component the sources belong to.
530    pub component: Cow<'a, str>,
531    /// The compression format of the sources index.
532    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/// A special type of [ReleaseFileEntry] that describes a `Translations` file.
588///
589/// These typically exist under paths named `<component>/i18n/Translation-<locale><compression>`.
590#[derive(Clone, Debug, PartialEq)]
591pub struct TranslationFileEntry<'a> {
592    /// The [ReleaseFileEntry] from which this instance was derived.
593    entry: ReleaseFileEntry<'a>,
594
595    /// The parsed component name (from the entry's path).
596    pub component: Cow<'a, str>,
597
598    /// The locale the translation is for.
599    pub locale: Cow<'a, str>,
600
601    /// File-level compression format being used.
602    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        // The component is the part up to `/i18n/Translation-`.
630        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/// A type of [ReleaseFileEntry] that describes a manifest of files with content digests.
672///
673/// This represents `MD5SUMS` and `SHA256SUMS` files which hold an additional list of files
674/// and their content manifests.
675#[derive(Clone, Debug, PartialEq)]
676pub struct FileManifestEntry<'a> {
677    /// The [ReleaseFileEntry] from which this instance was derived.
678    entry: ReleaseFileEntry<'a>,
679
680    /// The digest format stored in this file.
681    pub checksum: ChecksumType,
682
683    /// The root path for files in this manifest.
684    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/// A `[In]Release` file entry cast to its stronger type, if possible.
740#[derive(Debug)]
741pub enum ClassifiedReleaseFileEntry<'a> {
742    /// A `Contents` file.
743    Contents(ContentsFileEntry<'a>),
744    /// A `Packages` file.
745    Packages(PackagesFileEntry<'a>),
746    /// A `Sources` file.
747    Sources(SourcesFileEntry<'a>),
748    /// A nested `Release` file.
749    Release(ReleaseReleaseFileEntry<'a>),
750    /// An AppStream `Components` YAML file.
751    AppStreamComponents(AppStreamComponentsEntry<'a>),
752    /// An AppStream `Icons` file.
753    AppStreamIcons(AppStreamIconsFileEntry<'a>),
754    /// A `Translation` file.
755    Translation(TranslationFileEntry<'a>),
756    /// A `*SUMS` file containing content digests of additional files.
757    FileManifest(FileManifestEntry<'a>),
758    /// Some other file type.
759    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
796/// A Debian repository `Release` file.
797///
798/// Release files contain metadata and list the index files for a *repository*.
799/// They are effectively the entrypoint for defining a Debian repository and its
800/// content.
801///
802/// Instances are wrappers around a [ControlParagraph]. [Deref] and [DerefMut] are
803/// implemented to allow obtaining the inner [ControlParagraph]. [From] and [Into]
804/// are implemented to allow cheap type coercions. Note that converting from
805/// [ReleaseFile] to [ControlParagraph] may discard PGP cleartext signature data.
806pub struct ReleaseFile<'a> {
807    paragraph: ControlParagraph<'a>,
808
809    /// Parsed PGP signatures for this file.
810    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    /// Construct an instance by reading data from a reader.
844    ///
845    /// The source must be a Debian control file with exactly 1 paragraph.
846    ///
847    /// The source must not be PGP armored. i.e. do not feed it raw `InRelease`
848    /// files that begin with `-----BEGIN PGP SIGNED MESSAGE-----`.
849    pub fn from_reader<R: BufRead>(reader: R) -> Result<Self> {
850        let paragraphs = ControlParagraphReader::new(reader).collect::<Result<Vec<_>>>()?;
851
852        // A Release control file should have a single paragraph.
853        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    /// Construct an instance by reading data from a reader containing a PGP cleartext signature.
871    ///
872    /// This can be used to parse content from an `InRelease` file, which begins
873    /// with `-----BEGIN PGP SIGNED MESSAGE-----`.
874    ///
875    /// An error occurs if the PGP cleartext file is not well-formed or if a PGP parsing
876    /// error occurs.
877    ///
878    /// The PGP signature is NOT validated. The file will be parsed despite lack of
879    /// signature verification. This is conceptually insecure. But since Rust has memory
880    /// safety, some risk is prevented.
881    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    /// Obtain PGP signatures from this `InRelease` file.
892    pub fn signatures(&self) -> Option<&pgp_cleartext::CleartextSignatures> {
893        self.signatures.as_ref()
894    }
895
896    /// Description of this repository.
897    pub fn description(&self) -> Option<&str> {
898        self.field_str("Description")
899    }
900
901    /// Origin of the repository.
902    pub fn origin(&self) -> Option<&str> {
903        self.field_str("Origin")
904    }
905
906    /// Label for the repository.
907    pub fn label(&self) -> Option<&str> {
908        self.field_str("Label")
909    }
910
911    /// Version of this repository.
912    ///
913    /// Typically a sequence of `.` delimited integers.
914    pub fn version(&self) -> Option<&str> {
915        self.field_str("Version")
916    }
917
918    /// Suite of this repository.
919    ///
920    /// e.g. `stable`, `unstable`, `experimental`.
921    pub fn suite(&self) -> Option<&str> {
922        self.field_str("Suite")
923    }
924
925    /// Codename of this repository.
926    pub fn codename(&self) -> Option<&str> {
927        self.field_str("Codename")
928    }
929
930    /// Names of components within this repository.
931    ///
932    /// These are areas within the repository. Values may contain path characters.
933    /// e.g. `main`, `updates/main`.
934    pub fn components(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
935        self.iter_field_words("Components")
936    }
937
938    /// Debian machine architectures supported by this repository.
939    ///
940    /// e.g. `all`, `amd64`, `arm64`.
941    pub fn architectures(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
942        self.iter_field_words("Architectures")
943    }
944
945    /// Time the release file was created, as its raw string value.
946    pub fn date_str(&self) -> Option<&str> {
947        self.field_str("Date")
948    }
949
950    /// Time the release file was created, as a [DateTime].
951    ///
952    /// The timezone from the original file is always normalized to UTC.
953    pub fn date(&self) -> Option<Result<DateTime<Utc>>> {
954        self.field_datetime_rfc5322("Date")
955    }
956
957    /// Time the release file should be considered expired by the client, as its raw string value.
958    pub fn valid_until_str(&self) -> Option<&str> {
959        self.field_str("Valid-Until")
960    }
961
962    /// Time the release file should be considered expired by the client.
963    pub fn valid_until(&self) -> Option<Result<DateTime<Utc>>> {
964        self.field_datetime_rfc5322("Valid-Until")
965    }
966
967    /// Evaluated value for `NotAutomatic` field.
968    ///
969    /// `true` is returned iff the value is `yes`. `no` and other values result in `false`.
970    pub fn not_automatic(&self) -> Option<bool> {
971        self.field_bool("NotAutomatic")
972    }
973
974    /// Evaluated value for `ButAutomaticUpgrades` field.
975    ///
976    /// `true` is returned iff the value is `yes`. `no` and other values result in `false`.
977    pub fn but_automatic_upgrades(&self) -> Option<bool> {
978        self.field_bool("ButAutomaticUpgrades")
979    }
980
981    /// Whether to acquire files by hash.
982    pub fn acquire_by_hash(&self) -> Option<bool> {
983        self.field_bool("Acquire-By-Hash")
984    }
985
986    /// Obtain indexed files in this repository.
987    ///
988    /// Files are grouped by their checksum variant.
989    ///
990    /// If the specified checksum variant is present, [Some] is returned.
991    ///
992    /// The returned iterator emits [ReleaseFileEntry] instances. Entries are lazily
993    /// parsed as they are consumed from the iterator. Parse errors result in an [Err].
994    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                // Values are of form: <digest> <size> <path>
1001
1002                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                // Are paths with spaces allowed?
1009                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    /// Obtain indexed files in this repository classified to their type.
1024    ///
1025    /// This is like [Self::iter_index_files()] except it attempts classify each [ReleaseFileEntry]
1026    /// into a well-defined file type, returning a [ClassifiedReleaseFileEntry].
1027    ///
1028    /// If an entry doesn't map to a more well-defined type, [ClassifiedReleaseFileEntry::Other]
1029    /// will be emitted. If an error occurs when coercing an entry to its stronger type,
1030    /// [Err] will be emitted instead of [ClassifiedReleaseFileEntry::Other].
1031    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                    // This isn't the most efficient implementation or even the most semantically
1039                    // correct way to do it. But it should get the job done.
1040
1041                    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    /// Obtain `Contents` indices entries given a checksum flavor.
1131    ///
1132    /// This essentially looks for `Contents*` files in the file lists.
1133    ///
1134    /// The emitted entries have component and architecture values derived by the
1135    /// file paths. These values are not checked against the list of components
1136    /// and architectures defined by this file.
1137    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    /// Obtain `Packages` indices entries given a checksum flavor.
1156    ///
1157    /// This essentially looks for `Packages*` files in the file lists.
1158    ///
1159    /// The emitted entries have component and architecture values derived by the
1160    /// file paths. These values are not checked against the list of components
1161    /// and architectures defined by this file.
1162    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    /// Find a [PackagesFileEntry] given search constraints.
1181    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    /// Obtain `Sources` indices entries given a checksum flavor.
1211    ///
1212    /// This essentially looks for `Sources*` files in the file lists.
1213    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    /// Find a [SourcesFileEntry] given search constraints.
1232    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}