use {
crate::{
control::{ControlStanza, ParseError},
hash::Hash,
indexfile::IndexFile,
parse_size,
},
chrono::{DateTime, Utc},
itertools::{Either, Itertools},
ouroboros::self_referencing,
smallvec::SmallVec,
};
pub struct Release {
inner: ReleaseInner,
}
#[derive(Clone, PartialEq)]
pub struct ReleaseIndexFile<'a> {
pub path: &'a str,
pub fetch_path: Option<String>,
pub hash: Hash,
pub size: u64,
}
impl Clone for Release {
fn clone(&self) -> Self {
Release {
inner: ReleaseInnerTryBuilder {
data: self.inner.with_data(|d| d.clone()),
#[allow(clippy::borrowed_box)]
control_builder: |data: &'_ IndexFile| {
ControlStanza::parse(data).map_err(|err| {
ParseError::from(format!("error parsing release file: {}", err))
})
},
}
.try_build()
.expect("failed to clone release file"),
}
}
}
impl Release {
pub fn as_bytes(&self) -> &[u8] {
self.inner.with_data(|d| d.as_bytes())
}
pub fn len(&self) -> usize {
self.inner.with_data(|d| d.len())
}
pub fn is_empty(&self) -> bool {
self.inner.with_data(|d| d.is_empty())
}
fn files<'a, S: AsRef<str>>(
&'a self,
components: &'a [S],
hash_name: &'a str,
) -> Result<impl Iterator<Item = Result<(&'a str, &'a str, u64), ParseError>>, ParseError> {
let release_components = self.field("Components").unwrap_or("");
if components.iter().any(|c| {
!release_components.split_ascii_whitespace().any(|rc| {
rc.split('/')
.next_back()
.map(|s| s == c.as_ref())
.unwrap_or(false)
})
}) {
return Err(ParseError::from(format!(
"Component(s) {} not found in release components: {}",
components
.iter()
.map(|s| s.as_ref())
.filter(|s| !release_components
.split_ascii_whitespace()
.any(|rc| rc == *s))
.join(", "),
release_components
)));
}
let field = self.field(hash_name).ok_or_else(|| {
ParseError::from(format!("Field {} not found in the release file", hash_name,))
})?;
Ok(field
.lines()
.map(|l| l.trim())
.filter(|l| l != &"")
.map(|line| {
let parts: SmallVec<[&'_ str; 3]> = line.split_ascii_whitespace().collect();
if let [digest, size, path] = parts[..] {
let size = parse_size(size.as_bytes()).map_err(|err| {
ParseError::from(format!("invalid size: {:?} {}", size, err))
})?;
Ok((path, digest, size))
} else {
Err(ParseError::from(format!("Invalid release line: {}", line)))
}
}))
}
fn supports_acquire_by_hash(&self) -> bool {
self.field("Acquire-By-Hash")
.is_some_and(|value| value.trim_ascii().eq_ignore_ascii_case("yes"))
}
fn by_hash_path(path: &str, digest: &str) -> String {
match path.rsplit_once('/') {
Some((dir, _)) => format!("{dir}/by-hash/SHA256/{digest}"),
None => format!("by-hash/SHA256/{digest}"),
}
}
pub fn package_files<'a, S: AsRef<str>>(
&'a self,
components: &'a [S],
hash_name: &'a str,
arch: &'a str,
) -> Result<impl Iterator<Item = Result<ReleaseIndexFile<'a>, ParseError>>, ParseError> {
let use_by_hash =
self.supports_acquire_by_hash() && hash_name.eq_ignore_ascii_case("SHA256");
let fetch_all_arch = self
.field("No-Support-for-Architecture-all")
.is_none_or(|v| v.trim_ascii() != "Packages");
let arch = if fetch_all_arch && !components.is_empty() {
vec!["all", arch]
} else {
vec![arch]
};
let files = self.files(components, hash_name)?.try_fold(
arch.iter()
.flat_map(|arch| {
(if components.is_empty() {
Either::Left(std::iter::once(""))
} else {
Either::Right(components.iter().map(|c| c.as_ref()))
})
.into_iter()
.map(move |c| (c, *arch, None::<ReleaseIndexFile<'a>>))
})
.collect::<Vec<_>>(),
move |mut files, file| {
let (path, digest, size) = file?;
for (comp, arch, entry) in files.iter_mut() {
let pref = if comp.is_empty() {
path
} else {
let Some(pref) = path
.strip_prefix(*comp)
.and_then(|r| r.strip_prefix("/binary-"))
.and_then(|r| r.strip_prefix(*arch))
.and_then(|r| r.strip_prefix('/'))
else {
continue;
};
pref
};
if let Some("" | ".gz" | ".xz" | ".bz2" | ".zst" | ".zstd") =
pref.strip_prefix("Packages")
{
if size < entry.as_ref().map_or(u64::MAX, |file| file.size) {
let hash = Hash::from_hex(hash_name, digest).map_err(|err| {
ParseError::from(format!("invalid hash: {} {}", digest, err))
})?;
*entry = Some(ReleaseIndexFile {
path,
fetch_path: use_by_hash.then(|| Self::by_hash_path(path, digest)),
hash,
size,
});
}
}
}
Ok::<_, ParseError>(files)
},
)?;
Ok(files.into_iter().filter_map(|(comp, arch, entry)| {
if let Some(file) = entry {
if file.size == 0 {
None
} else {
Some(Ok(file))
}
} else if entry.is_none() && arch != "all" && !comp.is_empty() {
Some(Err(ParseError::from(format!(
"no Packages file found for component {} {}",
comp, arch,
))))
} else {
None
}
}))
}
pub fn source_files<'a, S: AsRef<str>>(
&'a self,
components: &'a [S],
hash_name: &'a str,
) -> Result<impl Iterator<Item = Result<ReleaseIndexFile<'a>, ParseError>>, ParseError> {
let use_by_hash =
self.supports_acquire_by_hash() && hash_name.eq_ignore_ascii_case("SHA256");
let files = self.files(components, hash_name)?.try_fold(
(if components.is_empty() {
Either::Left(std::iter::once(""))
} else {
Either::Right(components.iter().map(|c| c.as_ref()))
})
.into_iter()
.map(move |c| (c, None::<ReleaseIndexFile<'a>>))
.collect::<Vec<_>>(),
move |mut files, file| {
let (path, digest, size) = file?;
for (comp, entry) in files.iter_mut() {
let pref = if comp.is_empty() {
path
} else {
let Some(pref) = path
.strip_prefix(*comp)
.and_then(|r| r.strip_prefix("/source/"))
else {
continue;
};
pref
};
if let Some("" | ".gz" | ".xz" | ".bz2" | ".zst" | ".zstd") =
pref.strip_prefix("Sources")
{
if size < entry.as_ref().map_or(u64::MAX, |file| file.size) {
let hash = Hash::from_hex(hash_name, digest).map_err(|err| {
ParseError::from(format!("invalid hash: {} {}", digest, err))
})?;
*entry = Some(ReleaseIndexFile {
path,
fetch_path: use_by_hash.then(|| Self::by_hash_path(path, digest)),
hash,
size,
});
}
}
}
Ok::<_, ParseError>(files)
},
)?;
Ok(files.into_iter().filter_map(|(comp, entry)| {
if let Some(file) = entry {
if file.size == 0 {
None
} else {
Some(Ok(file))
}
} else if entry.is_none() {
Some(Err(ParseError::from(format!(
"no Sources file found for component {}",
comp,
))))
} else {
None
}
}))
}
fn field(&self, name: &str) -> Option<&str> {
self.inner.with_control(|ctrl| ctrl.field(name))
}
pub fn codename(&self) -> Option<&str> {
self.field("Codename")
}
pub fn origin(&self) -> Option<&str> {
self.field("Origin")
}
pub fn label(&self) -> Option<&str> {
self.field("Label")
}
pub fn components(&self) -> impl Iterator<Item = &str> {
self.field("Components").map_or_else(
|| "".split_ascii_whitespace(),
|line| line.split_ascii_whitespace(),
)
}
pub fn architectures(&self) -> impl Iterator<Item = &str> {
self.field("Architectures").map_or_else(
|| "".split_ascii_whitespace(),
|line| line.split_ascii_whitespace(),
)
}
pub fn description(&self) -> &str {
self.field("Description").unwrap_or("")
}
pub fn date(&self) -> Option<DateTime<Utc>> {
self.field("Date")
.and_then(|date| DateTime::parse_from_rfc2822(date).map(|t| t.to_utc()).ok())
}
pub fn valid_until(&self) -> Option<DateTime<Utc>> {
self.field("Valid-Until")
.and_then(|date| DateTime::parse_from_rfc2822(date).map(|t| t.to_utc()).ok())
}
pub fn new(data: IndexFile) -> Result<Release, ParseError> {
Ok(Release {
inner: ReleaseInnerTryBuilder {
data,
#[allow(clippy::borrowed_box)]
control_builder: |data: &'_ IndexFile| {
ControlStanza::parse(data).map_err(|err| {
ParseError::from(format!("error parsing release file: {}", err))
})
},
}
.try_build()?,
})
}
}
impl TryFrom<String> for Release {
type Error = ParseError;
fn try_from(str: String) -> Result<Self, Self::Error> {
Release::new(str.into())
}
}
impl TryFrom<Box<str>> for Release {
type Error = ParseError;
fn try_from(str: Box<str>) -> Result<Self, Self::Error> {
Release::new(str.into())
}
}
impl TryFrom<Vec<u8>> for Release {
type Error = ParseError;
fn try_from(vec: Vec<u8>) -> Result<Self, Self::Error> {
Release::new(
String::from_utf8(vec)
.map_err(|utf8err| ParseError::from(format!("invalid UTF-8: {}", utf8err)))?
.into(),
)
}
}
#[self_referencing]
struct ReleaseInner {
data: IndexFile,
#[borrows(data)]
#[covariant]
control: ControlStanza<'this>,
}