use {
crate::{
control::{ControlParagraph, ControlParagraphReader},
error::{DebianError, Result},
io::ContentDigest,
repository::Compression,
},
chrono::{DateTime, Utc},
pgp_cleartext::CleartextHasher,
std::{
borrow::Cow,
io::BufRead,
ops::{Deref, DerefMut},
str::FromStr,
},
};
pub const DATE_FORMAT: &str = "%a, %d %b %Y %H:%M:%S %z";
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ChecksumType {
Md5,
Sha1,
Sha256,
}
impl ChecksumType {
pub fn preferred_order() -> impl Iterator<Item = ChecksumType> {
[Self::Sha256, Self::Sha1, Self::Md5].into_iter()
}
pub fn field_name(&self) -> &'static str {
match self {
Self::Md5 => "MD5Sum",
Self::Sha1 => "SHA1",
Self::Sha256 => "SHA256",
}
}
pub fn new_hasher(&self) -> Box<dyn pgp::crypto::Hasher + Send> {
Box::new(match self {
Self::Md5 => CleartextHasher::md5(),
Self::Sha1 => CleartextHasher::sha1(),
Self::Sha256 => CleartextHasher::sha256(),
})
}
}
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct ReleaseFileEntry<'a> {
pub path: &'a str,
pub digest: ContentDigest,
pub size: u64,
}
impl<'a> ReleaseFileEntry<'a> {
pub fn by_hash_path(&self) -> String {
if let Some((prefix, _)) = self.path.rsplit_once('/') {
format!(
"{}/by-hash/{}/{}",
prefix,
self.digest.release_field_name(),
self.digest.digest_hex()
)
} else {
format!(
"by-hash/{}/{}",
self.digest.release_field_name(),
self.digest.digest_hex()
)
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AppStreamComponentsEntry<'a> {
entry: ReleaseFileEntry<'a>,
pub component: Cow<'a, str>,
pub architecture: Cow<'a, str>,
pub compression: Compression,
}
impl<'a> Deref for AppStreamComponentsEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for AppStreamComponentsEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<AppStreamComponentsEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: AppStreamComponentsEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for AppStreamComponentsEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let parts = entry.path.split('/').collect::<Vec<_>>();
let filename = *parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let suffix = filename
.strip_prefix("Components-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let (architecture, remainder) = suffix
.split_once('.')
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let compression = match remainder {
"yml" => Compression::None,
"yml.bz2" => Compression::Bzip2,
"yml.gz" => Compression::Gzip,
"yml.lzma" => Compression::Lzma,
"yml.xz" => Compression::Xz,
_ => {
return Err(DebianError::ReleaseIndicesEntryWrongType);
}
};
let component_end = entry
.path
.find("/dep11/Components-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let component = &entry.path[0..component_end];
Ok(Self {
entry,
component: component.into(),
architecture: architecture.into(),
compression,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AppStreamIconsFileEntry<'a> {
entry: ReleaseFileEntry<'a>,
pub component: Cow<'a, str>,
pub resolution: Cow<'a, str>,
pub compression: Compression,
}
impl<'a> Deref for AppStreamIconsFileEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for AppStreamIconsFileEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<AppStreamIconsFileEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: AppStreamIconsFileEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for AppStreamIconsFileEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let parts = entry.path.split('/').collect::<Vec<_>>();
let filename = *parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let suffix = filename
.strip_prefix("icons-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let (resolution, remainder) = suffix
.split_once('.')
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let compression = match remainder {
"tar" => Compression::None,
"tar.bz2" => Compression::Bzip2,
"tar.gz" => Compression::Gzip,
"tar.lzma" => Compression::Lzma,
"tar.xz" => Compression::Xz,
_ => {
return Err(DebianError::ReleaseIndicesEntryWrongType);
}
};
let component_end = entry
.path
.find("/dep11/icons-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let component = &entry.path[0..component_end];
Ok(Self {
entry,
component: component.into(),
resolution: resolution.into(),
compression,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ContentsFileEntry<'a> {
entry: ReleaseFileEntry<'a>,
pub component: Cow<'a, str>,
pub architecture: Cow<'a, str>,
pub compression: Compression,
pub is_installer: bool,
}
impl<'a> Deref for ContentsFileEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for ContentsFileEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<ContentsFileEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: ContentsFileEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for ContentsFileEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let parts = entry.path.split('/').collect::<Vec<_>>();
let filename = *parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let suffix = filename
.strip_prefix("Contents-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let (architecture, compression) = if let Some(v) = suffix.strip_suffix(".gz") {
(v, Compression::Gzip)
} else {
(suffix, Compression::None)
};
let (architecture, is_installer) = if let Some(v) = architecture.strip_prefix("udeb-") {
(v, true)
} else {
(architecture, false)
};
let component = &entry.path[..entry.path.len() - filename.len() - 1];
Ok(Self {
entry,
component: component.into(),
architecture: architecture.into(),
compression,
is_installer,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PackagesFileEntry<'a> {
entry: ReleaseFileEntry<'a>,
pub component: Cow<'a, str>,
pub architecture: Cow<'a, str>,
pub compression: Compression,
pub is_installer: bool,
}
impl<'a> Deref for PackagesFileEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for PackagesFileEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<PackagesFileEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: PackagesFileEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for PackagesFileEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let parts = entry.path.split('/').collect::<Vec<_>>();
let compression = match *parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?
{
"Packages" => Compression::None,
"Packages.xz" => Compression::Xz,
"Packages.gz" => Compression::Gzip,
"Packages.bz2" => Compression::Bzip2,
"Packages.lzma" => Compression::Lzma,
_ => {
return Err(DebianError::ReleaseIndicesEntryWrongType);
}
};
let architecture_component = *parts
.iter()
.rev()
.nth(1)
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let search = &entry.path[..entry.path.len()
- parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?
.len()
- 1];
let component = &search[0..search
.rfind('/')
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?];
let architecture = architecture_component
.strip_prefix("binary-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let (component, is_udeb) =
if let Some(component) = component.strip_suffix("/debian-installer") {
(component, true)
} else {
(component, false)
};
Ok(Self {
entry,
component: component.into(),
architecture: architecture.into(),
compression,
is_installer: is_udeb,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReleaseReleaseFileEntry<'a> {
entry: ReleaseFileEntry<'a>,
}
impl<'a> Deref for ReleaseReleaseFileEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for ReleaseReleaseFileEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<ReleaseReleaseFileEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: ReleaseReleaseFileEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for ReleaseReleaseFileEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let parts = entry.path.split('/').collect::<Vec<_>>();
if *parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?
!= "Release"
{
return Err(DebianError::ReleaseIndicesEntryWrongType);
}
Ok(Self { entry })
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SourcesFileEntry<'a> {
entry: ReleaseFileEntry<'a>,
pub component: Cow<'a, str>,
pub compression: Compression,
}
impl<'a> Deref for SourcesFileEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for SourcesFileEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<SourcesFileEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: SourcesFileEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for SourcesFileEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let parts = entry.path.split('/').collect::<Vec<_>>();
let compression = match *parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?
{
"Sources" => Compression::None,
"Sources.gz" => Compression::Gzip,
"Sources.xz" => Compression::Xz,
"Sources.bz2" => Compression::Bzip2,
"Sources.lzma" => Compression::Lzma,
_ => {
return Err(DebianError::ReleaseIndicesEntryWrongType);
}
};
let component = *parts
.first()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
Ok(Self {
entry,
component: component.into(),
compression,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TranslationFileEntry<'a> {
entry: ReleaseFileEntry<'a>,
pub component: Cow<'a, str>,
pub locale: Cow<'a, str>,
pub compression: Compression,
}
impl<'a> Deref for TranslationFileEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for TranslationFileEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<TranslationFileEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: TranslationFileEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for TranslationFileEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let component_end = entry
.path
.find("/i18n/Translation-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let component = &entry.path[0..component_end];
let parts = entry.path.split('/').collect::<Vec<_>>();
let filename = parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let remainder = filename
.strip_prefix("Translation-")
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let (locale, compression) = if let Some((locale, extension)) = remainder.split_once('.') {
let compression = match extension {
"gz" => Compression::Gzip,
"bz2" => Compression::Bzip2,
"lzma" => Compression::Lzma,
"xz" => Compression::Xz,
_ => {
return Err(DebianError::ReleaseIndicesEntryWrongType);
}
};
(locale, compression)
} else {
(remainder, Compression::None)
};
Ok(Self {
entry,
component: component.into(),
locale: locale.into(),
compression,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FileManifestEntry<'a> {
entry: ReleaseFileEntry<'a>,
pub checksum: ChecksumType,
pub root_path: Cow<'a, str>,
}
impl<'a> Deref for FileManifestEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl<'a> DerefMut for FileManifestEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entry
}
}
impl<'a> From<FileManifestEntry<'a>> for ReleaseFileEntry<'a> {
fn from(v: FileManifestEntry<'a>) -> Self {
v.entry
}
}
impl<'a> TryFrom<ReleaseFileEntry<'a>> for FileManifestEntry<'a> {
type Error = DebianError;
fn try_from(entry: ReleaseFileEntry<'a>) -> std::result::Result<Self, Self::Error> {
let parts = entry.path.split('/').collect::<Vec<_>>();
let filename = *parts
.last()
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?;
let checksum = match filename {
"MD5SUMS" => ChecksumType::Md5,
"SHA256SUMS" => ChecksumType::Sha256,
_ => {
return Err(DebianError::ReleaseIndicesEntryWrongType);
}
};
let root_path = entry
.path
.rsplit_once('/')
.ok_or(DebianError::ReleaseIndicesEntryWrongType)?
.0;
Ok(Self {
entry,
checksum,
root_path: root_path.into(),
})
}
}
#[derive(Debug)]
pub enum ClassifiedReleaseFileEntry<'a> {
Contents(ContentsFileEntry<'a>),
Packages(PackagesFileEntry<'a>),
Sources(SourcesFileEntry<'a>),
Release(ReleaseReleaseFileEntry<'a>),
AppStreamComponents(AppStreamComponentsEntry<'a>),
AppStreamIcons(AppStreamIconsFileEntry<'a>),
Translation(TranslationFileEntry<'a>),
FileManifest(FileManifestEntry<'a>),
Other(ReleaseFileEntry<'a>),
}
impl<'a> Deref for ClassifiedReleaseFileEntry<'a> {
type Target = ReleaseFileEntry<'a>;
fn deref(&self) -> &Self::Target {
match self {
Self::Contents(v) => &v.entry,
Self::Packages(v) => &v.entry,
Self::Sources(v) => &v.entry,
Self::Release(v) => &v.entry,
Self::AppStreamComponents(v) => &v.entry,
Self::AppStreamIcons(v) => &v.entry,
Self::Translation(v) => &v.entry,
Self::FileManifest(v) => &v.entry,
Self::Other(v) => v,
}
}
}
impl<'a> DerefMut for ClassifiedReleaseFileEntry<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Contents(v) => &mut v.entry,
Self::Packages(v) => &mut v.entry,
Self::Sources(v) => &mut v.entry,
Self::Release(v) => &mut v.entry,
Self::AppStreamComponents(v) => &mut v.entry,
Self::AppStreamIcons(v) => &mut v.entry,
Self::Translation(v) => &mut v.entry,
Self::FileManifest(v) => &mut v.entry,
Self::Other(v) => v,
}
}
}
pub struct ReleaseFile<'a> {
paragraph: ControlParagraph<'a>,
signatures: Option<pgp_cleartext::CleartextSignatures>,
}
impl<'a> From<ControlParagraph<'a>> for ReleaseFile<'a> {
fn from(paragraph: ControlParagraph<'a>) -> Self {
Self {
paragraph,
signatures: None,
}
}
}
impl<'a> From<ReleaseFile<'a>> for ControlParagraph<'a> {
fn from(release: ReleaseFile<'a>) -> Self {
release.paragraph
}
}
impl<'a> Deref for ReleaseFile<'a> {
type Target = ControlParagraph<'a>;
fn deref(&self) -> &Self::Target {
&self.paragraph
}
}
impl<'a> DerefMut for ReleaseFile<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.paragraph
}
}
impl<'a> ReleaseFile<'a> {
pub fn from_reader<R: BufRead>(reader: R) -> Result<Self> {
let paragraphs = ControlParagraphReader::new(reader).collect::<Result<Vec<_>>>()?;
if paragraphs.len() != 1 {
return Err(DebianError::ReleaseControlParagraphMismatch(
paragraphs.len(),
));
}
let paragraph = paragraphs
.into_iter()
.next()
.expect("validated paragraph count above");
Ok(Self {
paragraph,
signatures: None,
})
}
pub fn from_armored_reader<R: BufRead>(reader: R) -> Result<Self> {
let reader = pgp_cleartext::CleartextSignatureReader::new(reader);
let mut reader = std::io::BufReader::new(reader);
let mut slf = Self::from_reader(&mut reader)?;
slf.signatures = Some(reader.into_inner().finalize());
Ok(slf)
}
pub fn signatures(&self) -> Option<&pgp_cleartext::CleartextSignatures> {
self.signatures.as_ref()
}
pub fn description(&self) -> Option<&str> {
self.field_str("Description")
}
pub fn origin(&self) -> Option<&str> {
self.field_str("Origin")
}
pub fn label(&self) -> Option<&str> {
self.field_str("Label")
}
pub fn version(&self) -> Option<&str> {
self.field_str("Version")
}
pub fn suite(&self) -> Option<&str> {
self.field_str("Suite")
}
pub fn codename(&self) -> Option<&str> {
self.field_str("Codename")
}
pub fn components(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
self.iter_field_words("Components")
}
pub fn architectures(&self) -> Option<Box<(dyn Iterator<Item = &str> + '_)>> {
self.iter_field_words("Architectures")
}
pub fn date_str(&self) -> Option<&str> {
self.field_str("Date")
}
pub fn date(&self) -> Option<Result<DateTime<Utc>>> {
self.field_datetime_rfc5322("Date")
}
pub fn valid_until_str(&self) -> Option<&str> {
self.field_str("Valid-Until")
}
pub fn valid_until(&self) -> Option<Result<DateTime<Utc>>> {
self.field_datetime_rfc5322("Valid-Until")
}
pub fn not_automatic(&self) -> Option<bool> {
self.field_bool("NotAutomatic")
}
pub fn but_automatic_upgrades(&self) -> Option<bool> {
self.field_bool("ButAutomaticUpgrades")
}
pub fn acquire_by_hash(&self) -> Option<bool> {
self.field_bool("Acquire-By-Hash")
}
pub fn iter_index_files(
&self,
checksum: ChecksumType,
) -> Option<Box<(dyn Iterator<Item = Result<ReleaseFileEntry<'_>>> + '_)>> {
if let Some(iter) = self.iter_field_lines(checksum.field_name()) {
Some(Box::new(iter.map(move |v| {
let mut parts = v.split_ascii_whitespace();
let digest = parts.next().ok_or(DebianError::ReleaseMissingDigest)?;
let size = parts.next().ok_or(DebianError::ReleaseMissingSize)?;
let path = parts.next().ok_or(DebianError::ReleaseMissingPath)?;
if parts.next().is_some() {
return Err(DebianError::ReleasePathWithSpaces(v.to_string()));
}
let digest = ContentDigest::from_hex_digest(checksum, digest)?;
let size = u64::from_str(size)?;
Ok(ReleaseFileEntry { path, digest, size })
})))
} else {
None
}
}
pub fn iter_classified_index_files(
&self,
checksum: ChecksumType,
) -> Option<Box<(dyn Iterator<Item = Result<ClassifiedReleaseFileEntry<'_>>> + '_)>> {
if let Some(iter) = self.iter_index_files(checksum) {
Some(Box::new(iter.map(|entry| match entry {
Ok(entry) => {
match ContentsFileEntry::try_from(entry.clone()) {
Ok(contents) => {
return Ok(ClassifiedReleaseFileEntry::Contents(contents));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
match FileManifestEntry::try_from(entry.clone()) {
Ok(entry) => {
return Ok(ClassifiedReleaseFileEntry::FileManifest(entry));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
match PackagesFileEntry::try_from(entry.clone()) {
Ok(packages) => {
return Ok(ClassifiedReleaseFileEntry::Packages(packages));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
match ReleaseReleaseFileEntry::try_from(entry.clone()) {
Ok(release) => {
return Ok(ClassifiedReleaseFileEntry::Release(release));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
match AppStreamComponentsEntry::try_from(entry.clone()) {
Ok(components) => {
return Ok(ClassifiedReleaseFileEntry::AppStreamComponents(components));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
match AppStreamIconsFileEntry::try_from(entry.clone()) {
Ok(icons) => {
return Ok(ClassifiedReleaseFileEntry::AppStreamIcons(icons));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
match TranslationFileEntry::try_from(entry.clone()) {
Ok(entry) => {
return Ok(ClassifiedReleaseFileEntry::Translation(entry));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
match SourcesFileEntry::try_from(entry.clone()) {
Ok(sources) => {
return Ok(ClassifiedReleaseFileEntry::Sources(sources));
}
Err(DebianError::ReleaseIndicesEntryWrongType) => {}
Err(e) => {
return Err(e);
}
}
Ok(ClassifiedReleaseFileEntry::Other(entry))
}
Err(e) => Err(e),
})))
} else {
None
}
}
pub fn iter_contents_indices(
&self,
checksum: ChecksumType,
) -> Option<Box<(dyn Iterator<Item = Result<ContentsFileEntry<'_>>> + '_)>> {
if let Some(iter) = self.iter_index_files(checksum) {
Some(Box::new(iter.filter_map(|entry| match entry {
Ok(entry) => match ContentsFileEntry::try_from(entry) {
Ok(v) => Some(Ok(v)),
Err(DebianError::ReleaseIndicesEntryWrongType) => None,
Err(e) => Some(Err(e)),
},
Err(e) => Some(Err(e)),
})))
} else {
None
}
}
pub fn iter_packages_indices(
&self,
checksum: ChecksumType,
) -> Option<Box<(dyn Iterator<Item = Result<PackagesFileEntry<'_>>> + '_)>> {
if let Some(iter) = self.iter_index_files(checksum) {
Some(Box::new(iter.filter_map(|entry| match entry {
Ok(entry) => match PackagesFileEntry::try_from(entry) {
Ok(v) => Some(Ok(v)),
Err(DebianError::ReleaseIndicesEntryWrongType) => None,
Err(e) => Some(Err(e)),
},
Err(e) => Some(Err(e)),
})))
} else {
None
}
}
pub fn find_packages_indices(
&self,
checksum: ChecksumType,
compression: Compression,
component: &str,
arch: &str,
is_installer: bool,
) -> Option<PackagesFileEntry<'_>> {
if let Some(mut iter) = self.iter_packages_indices(checksum) {
iter.find_map(|entry| {
if let Ok(entry) = entry {
if entry.component == component
&& entry.architecture == arch
&& entry.is_installer == is_installer
&& entry.compression == compression
{
Some(entry)
} else {
None
}
} else {
None
}
})
} else {
None
}
}
pub fn iter_sources_indices(
&self,
checksum: ChecksumType,
) -> Option<Box<(dyn Iterator<Item = Result<SourcesFileEntry<'_>>> + '_)>> {
if let Some(iter) = self.iter_index_files(checksum) {
Some(Box::new(iter.filter_map(|entry| match entry {
Ok(entry) => match SourcesFileEntry::try_from(entry) {
Ok(v) => Some(Ok(v)),
Err(DebianError::ReleaseIndicesEntryWrongType) => None,
Err(e) => Some(Err(e)),
},
Err(e) => Some(Err(e)),
})))
} else {
None
}
}
pub fn find_sources_indices(
&self,
checksum: ChecksumType,
compression: Compression,
component: &str,
) -> Option<SourcesFileEntry<'_>> {
if let Some(mut iter) = self.iter_sources_indices(checksum) {
iter.find_map(|entry| {
if let Ok(entry) = entry {
if entry.component == component && entry.compression == compression {
Some(entry)
} else {
None
}
} else {
None
}
})
} else {
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_bullseye_release() -> Result<()> {
let mut reader =
std::io::Cursor::new(include_bytes!("../testdata/release-debian-bullseye"));
let release = ReleaseFile::from_reader(&mut reader)?;
assert_eq!(
release.description(),
Some("Debian 11.1 Released 09 October 2021")
);
assert_eq!(release.origin(), Some("Debian"));
assert_eq!(release.label(), Some("Debian"));
assert_eq!(release.version(), Some("11.1"));
assert_eq!(release.suite(), Some("stable"));
assert_eq!(release.codename(), Some("bullseye"));
assert_eq!(
release.components().unwrap().collect::<Vec<_>>(),
vec!["main", "contrib", "non-free"]
);
assert_eq!(
release.architectures().unwrap().collect::<Vec<_>>(),
vec![
"all", "amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el",
"s390x"
]
);
assert_eq!(release.date_str(), Some("Sat, 09 Oct 2021 09:34:56 UTC"));
assert_eq!(
release.date().unwrap()?,
DateTime::<Utc>::from_utc(
chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd(2021, 10, 9),
chrono::NaiveTime::from_hms(9, 34, 56)
),
Utc
)
);
assert!(release.valid_until_str().is_none());
let entries = release
.iter_index_files(ChecksumType::Md5)
.unwrap()
.collect::<Result<Vec<_>>>()?;
assert_eq!(entries.len(), 600);
assert_eq!(
entries[0],
ReleaseFileEntry {
path: "contrib/Contents-all",
digest: ContentDigest::md5_hex("7fdf4db15250af5368cc52a91e8edbce").unwrap(),
size: 738242,
}
);
assert_eq!(
entries[0].by_hash_path(),
"contrib/by-hash/MD5Sum/7fdf4db15250af5368cc52a91e8edbce"
);
assert_eq!(
entries[1],
ReleaseFileEntry {
path: "contrib/Contents-all.gz",
digest: ContentDigest::md5_hex("cbd7bc4d3eb517ac2b22f929dfc07b47").unwrap(),
size: 57319,
}
);
assert_eq!(
entries[1].by_hash_path(),
"contrib/by-hash/MD5Sum/cbd7bc4d3eb517ac2b22f929dfc07b47"
);
assert_eq!(
entries[599],
ReleaseFileEntry {
path: "non-free/source/Sources.xz",
digest: ContentDigest::md5_hex("e3830f6fc5a946b5a5b46e8277e1d86f").unwrap(),
size: 80488,
}
);
assert_eq!(
entries[599].by_hash_path(),
"non-free/source/by-hash/MD5Sum/e3830f6fc5a946b5a5b46e8277e1d86f"
);
assert!(release.iter_index_files(ChecksumType::Sha1).is_none());
let entries = release
.iter_index_files(ChecksumType::Sha256)
.unwrap()
.collect::<Result<Vec<_>>>()?;
assert_eq!(entries.len(), 600);
assert_eq!(
entries[0],
ReleaseFileEntry {
path: "contrib/Contents-all",
digest: ContentDigest::sha256_hex(
"3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63"
)
.unwrap(),
size: 738242,
}
);
assert_eq!(entries[0].by_hash_path(), "contrib/by-hash/SHA256/3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63");
assert_eq!(
entries[1],
ReleaseFileEntry {
path: "contrib/Contents-all.gz",
digest: ContentDigest::sha256_hex(
"3e9a121d599b56c08bc8f144e4830807c77c29d7114316d6984ba54695d3db7b"
)
.unwrap(),
size: 57319,
}
);
assert_eq!(entries[1].by_hash_path(), "contrib/by-hash/SHA256/3e9a121d599b56c08bc8f144e4830807c77c29d7114316d6984ba54695d3db7b");
assert_eq!(
entries[599],
ReleaseFileEntry {
digest: ContentDigest::sha256_hex(
"30f3f996941badb983141e3b29b2ed5941d28cf81f9b5f600bb48f782d386fc7"
)
.unwrap(),
size: 80488,
path: "non-free/source/Sources.xz",
}
);
assert_eq!(entries[599].by_hash_path(), "non-free/source/by-hash/SHA256/30f3f996941badb983141e3b29b2ed5941d28cf81f9b5f600bb48f782d386fc7");
const EXPECTED_CONTENTS: usize = 126;
const EXPECTED_PACKAGES: usize = 180;
const EXPECTED_SOURCES: usize = 9;
const EXPECTED_RELEASE: usize = 63;
const EXPECTED_APPSTREAM_COMPONENTS: usize = 72;
const EXPECTED_APPSTREAM_ICONS: usize = 18;
const EXPECTED_TRANSLATION: usize = 78;
const EXPECTED_FILEMANIFEST: usize = 54;
const EXPECTED_OTHER: usize = 600
- EXPECTED_CONTENTS
- EXPECTED_PACKAGES
- EXPECTED_SOURCES
- EXPECTED_RELEASE
- EXPECTED_APPSTREAM_COMPONENTS
- EXPECTED_APPSTREAM_ICONS
- EXPECTED_TRANSLATION
- EXPECTED_FILEMANIFEST;
assert_eq!(EXPECTED_OTHER, 0);
let entries = release
.iter_classified_index_files(ChecksumType::Sha256)
.unwrap()
.collect::<Result<Vec<_>>>()?;
assert_eq!(entries.len(), 600);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Contents(_)))
.count(),
EXPECTED_CONTENTS
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Packages(_)))
.count(),
EXPECTED_PACKAGES
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Sources(_)))
.count(),
EXPECTED_SOURCES
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Release(_)))
.count(),
EXPECTED_RELEASE
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::AppStreamComponents(_)))
.count(),
EXPECTED_APPSTREAM_COMPONENTS
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::AppStreamIcons(_)))
.count(),
EXPECTED_APPSTREAM_ICONS
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Translation(_)))
.count(),
EXPECTED_TRANSLATION
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::FileManifest(_)))
.count(),
EXPECTED_FILEMANIFEST
);
assert_eq!(
entries
.iter()
.filter(|entry| matches!(entry, ClassifiedReleaseFileEntry::Other(_)))
.count(),
EXPECTED_OTHER
);
let contents = release
.iter_contents_indices(ChecksumType::Sha256)
.unwrap()
.collect::<Result<Vec<_>>>()?;
assert_eq!(contents.len(), EXPECTED_CONTENTS);
assert_eq!(
contents[0],
ContentsFileEntry {
entry: ReleaseFileEntry {
path: "contrib/Contents-all",
digest: ContentDigest::sha256_hex(
"3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63"
)
.unwrap(),
size: 738242,
},
component: "contrib".into(),
architecture: "all".into(),
compression: Compression::None,
is_installer: false
}
);
assert_eq!(
contents[1],
ContentsFileEntry {
entry: ReleaseFileEntry {
path: "contrib/Contents-all.gz",
digest: ContentDigest::sha256_hex(
"3e9a121d599b56c08bc8f144e4830807c77c29d7114316d6984ba54695d3db7b"
)
.unwrap(),
size: 57319,
},
component: "contrib".into(),
architecture: "all".into(),
compression: Compression::Gzip,
is_installer: false
}
);
assert_eq!(
contents[24],
ContentsFileEntry {
entry: ReleaseFileEntry {
path: "contrib/Contents-udeb-amd64",
digest: ContentDigest::sha256_hex(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
.unwrap(),
size: 0,
},
component: "contrib".into(),
architecture: "amd64".into(),
compression: Compression::None,
is_installer: true
}
);
let packages = release
.iter_packages_indices(ChecksumType::Sha256)
.unwrap()
.collect::<Result<Vec<_>>>()?;
assert_eq!(packages.len(), EXPECTED_PACKAGES);
assert_eq!(
packages[0],
PackagesFileEntry {
entry: ReleaseFileEntry {
path: "contrib/binary-all/Packages",
digest: ContentDigest::sha256_hex(
"48cfe101cd84f16baf720b99e8f2ff89fd7e063553966d8536b472677acb82f0"
)
.unwrap(),
size: 103223,
},
component: "contrib".into(),
architecture: "all".into(),
compression: Compression::None,
is_installer: false
}
);
assert_eq!(
packages[1],
PackagesFileEntry {
entry: ReleaseFileEntry {
path: "contrib/binary-all/Packages.gz",
digest: ContentDigest::sha256_hex(
"86057fcd3eff667ec8e3fbabb2a75e229f5e99f39ace67ff0db4a8509d0707e4"
)
.unwrap(),
size: 27334,
},
component: "contrib".into(),
architecture: "all".into(),
compression: Compression::Gzip,
is_installer: false
}
);
assert_eq!(
packages[2],
PackagesFileEntry {
entry: ReleaseFileEntry {
path: "contrib/binary-all/Packages.xz",
digest: ContentDigest::sha256_hex(
"706c840235798e098d4d6013d1dabbc967f894d0ffa02c92ac959dcea85ddf54"
)
.unwrap(),
size: 23912,
},
component: "contrib".into(),
architecture: "all".into(),
compression: Compression::Xz,
is_installer: false
}
);
let udeps = packages
.into_iter()
.filter(|x| x.is_installer)
.collect::<Vec<_>>();
assert_eq!(udeps.len(), 90);
assert_eq!(
udeps[0],
PackagesFileEntry {
entry: ReleaseFileEntry {
path: "contrib/debian-installer/binary-all/Packages",
digest: ContentDigest::sha256_hex(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
.unwrap(),
size: 0,
},
component: "contrib".into(),
architecture: "all".into(),
compression: Compression::None,
is_installer: true
}
);
let sources = release
.iter_sources_indices(ChecksumType::Sha256)
.unwrap()
.collect::<Result<Vec<_>>>()?;
assert_eq!(sources.len(), EXPECTED_SOURCES);
let entry = release
.find_sources_indices(ChecksumType::Sha256, Compression::Xz, "main")
.unwrap();
assert_eq!(
entry,
SourcesFileEntry {
entry: ReleaseFileEntry {
path: "main/source/Sources.xz",
digest: ContentDigest::sha256_hex(
"1801d18c1135168d5dd86a8cb85fb5cd5bd81e16174acc25d900dee11389e9cd"
)
.unwrap(),
size: 8616784,
},
component: "main".into(),
compression: Compression::Xz
}
);
Ok(())
}
fn bullseye_signing_key() -> pgp::SignedPublicKey {
crate::signing_key::DistroSigningKey::Debian11Release.public_key()
}
#[test]
fn parse_bullseye_inrelease() -> Result<()> {
let reader = std::io::Cursor::new(include_bytes!("../testdata/inrelease-debian-bullseye"));
let release = ReleaseFile::from_armored_reader(reader)?;
let signing_key = bullseye_signing_key();
assert_eq!(release.signatures.unwrap().verify(&signing_key).unwrap(), 1);
Ok(())
}
#[test]
fn bad_signature_rejection() -> Result<()> {
let reader = std::io::Cursor::new(
include_str!("../testdata/inrelease-debian-bullseye").replace(
"d41d8cd98f00b204e9800998ecf8427e",
"d41d8cd98f00b204e9800998ecf80000",
),
);
let release = ReleaseFile::from_armored_reader(reader)?;
let signing_key = bullseye_signing_key();
assert!(release.signatures.unwrap().verify(&signing_key).is_err());
Ok(())
}
}