#[cfg(not(test))]
use log::{info, warn};
#[cfg(test)]
use std::{println as warn, println as info};
use chrono::DateTime;
use chrono::FixedOffset;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::signature::verify_in_release;
use crate::util::{download, get_etag};
use crate::Architecture;
use crate::Distro;
use crate::Link;
use crate::LinkHash;
use crate::{Error, ErrorType, Result};
#[derive(Debug, Deserialize, Serialize)]
pub struct Release {
hash: Option<String>,
pub origin: Option<String>,
pub label: Option<String>,
pub suite: Option<String>,
pub version: Option<String>,
pub codename: Option<String>,
pub date: Option<DateTime<FixedOffset>>,
pub valid_until: Option<DateTime<FixedOffset>>,
pub architectures: Vec<Architecture>,
pub components: Vec<String>,
pub description: Option<String>,
pub links: HashMap<String, Link>,
pub acquire_by_hash: bool,
pub signed_by: Vec<String>,
pub changelogs: Option<String>,
pub snapshots: Option<String>,
pub distro: Distro,
pub issues: Vec<Error>,
}
impl Release {
fn new(distro: &Distro) -> Release {
Release {
hash: None,
origin: None,
label: None,
suite: None,
version: None,
codename: None,
date: None,
valid_until: None,
architectures: Vec::new(),
components: Vec::new(),
description: None,
links: HashMap::new(),
acquire_by_hash: false, signed_by: Vec::new(),
changelogs: None,
snapshots: None,
distro: distro.clone(),
issues: Vec::new(),
}
}
pub async fn from_distro(distro: &Distro) -> Result<Release> {
let url = distro.in_release_url()?;
let content = download(&url).await?;
let content = verify_in_release(content, distro).await?;
let mut section = ReleaseSection::Keywords;
let mut release = Release::new(distro);
for line in content.lines() {
if line.trim().is_empty() {
continue;
}
if line.starts_with("---") {
if line.starts_with("-----BEGIN PGP SIGNATURE") {
break;
} else {
continue;
}
} else if !line.starts_with(" ") {
section = ReleaseSection::Keywords;
}
match §ion {
ReleaseSection::Keywords => {
if !line.contains(":") {
return Err(Error::new(
&format!("Invalid line! {line}"),
ErrorType::InReleaseFormat,
));
}
let mut parts = line.splitn(2, ":");
let keyword = parts.next().unwrap();
let value = parts.next().unwrap();
let keyword = keyword.to_lowercase();
let plain_value = value.trim();
let value = Some(plain_value.to_string());
if keyword == "hash" {
release.hash = value;
} else if keyword == "origin" {
release.origin = value;
} else if keyword == "label" {
release.label = value;
} else if keyword == "suite" {
release.suite = value;
} else if keyword == "version" {
release.version = value;
} else if keyword == "codename" {
release.codename = value;
} else if keyword == "description" {
release.description = value;
} else if keyword == "changelogs" {
release.changelogs = value;
} else if keyword == "snapshots" {
release.snapshots = value;
} else if keyword == "date" {
let plain_value = plain_value.replace("UTC", "+0000");
release.date = match DateTime::parse_from_rfc2822(&plain_value) {
Ok(date) => Some(date),
Err(e) => {
warn!("Parsing Release date \"{plain_value}\" failed! {e}");
None
}
}
} else if keyword == "valid-until" {
let plain_value = plain_value.replace("UTC", "+0000");
release.valid_until = match DateTime::parse_from_rfc2822(&plain_value) {
Ok(date) => Some(date),
Err(e) => {
warn!("Parsing Release valid until failed! {e}");
None
}
}
} else if keyword == "architectures" {
release.architectures = plain_value
.split(" ")
.filter_map(|e| match Architecture::from_str(e) {
Ok(arch) => Some(arch),
Err(e) => {
warn!("Parsing architecture {e} failed!");
None
}
})
.collect();
} else if keyword == "components" {
release.components = plain_value
.split(" ")
.filter(|e| !e.trim().is_empty())
.map(|e| e.to_string())
.collect();
} else if keyword == "acquire-by-hash" {
release.acquire_by_hash = match plain_value.to_lowercase().as_str() {
"yes" => true,
_ => false,
}
} else if keyword == "signed-by" {
release.signed_by = plain_value
.split(",")
.map(|e| e.trim().to_string())
.collect();
} else if keyword == "md5sum" {
section = ReleaseSection::HashMD5;
} else if keyword == "sha1" {
section = ReleaseSection::HashSHA1;
} else if keyword == "sha256" {
section = ReleaseSection::HashSHA256;
} else if keyword == "sha512" {
section = ReleaseSection::HashSHA512;
} else {
warn!("Unknown keyword: {keyword} of line {line}!");
}
}
section => {
let link = match Link::form_release(line, distro) {
Ok(link) => link,
Err(e) => {
release.issues.push(e);
continue;
}
};
let url = link.url.clone();
if !release.links.contains_key(&url) {
release.links.insert(url.clone(), link);
}
let link = release.links.get_mut(&url).unwrap();
match section {
ReleaseSection::HashMD5 => match link.add_hash(line, LinkHash::Md5) {
Ok(_) => {}
Err(e) => {
release.issues.push(e);
}
},
ReleaseSection::HashSHA1 => match link.add_hash(line, LinkHash::Sha1) {
Ok(_) => {}
Err(e) => {
release.issues.push(e);
}
},
ReleaseSection::HashSHA256 => match link.add_hash(line, LinkHash::Sha256) {
Ok(_) => {}
Err(e) => {
release.issues.push(e);
}
},
ReleaseSection::HashSHA512 => match link.add_hash(line, LinkHash::Sha512) {
Ok(_) => {}
Err(e) => {
release.issues.push(e);
}
},
_ => {}
};
}
}
}
Ok(release)
}
pub fn check_compliance(&self) -> Result<()> {
if self.components.is_empty() {
return Err(Error::new(
"No components provided.",
ErrorType::InReleaseStandard,
));
}
if self.architectures.is_empty() {
return Err(Error::new(
"No architectures provided.",
ErrorType::InReleaseStandard,
));
}
if self.suite == None && self.codename == None {
return Err(Error::new(
"Neither suite nor codename provided.",
ErrorType::InReleaseStandard,
));
}
if self.date == None {
return Err(Error::new(
"No date provided.",
ErrorType::InReleaseStandard,
));
}
for key in self.links.keys() {
let link = self.links.get(key).unwrap();
if !link.hashes.contains_key(&LinkHash::Sha256) {
return Err(Error::new(
&format!("No SHA256 hash provided for URL {key}."),
ErrorType::InReleaseStandard,
));
}
}
Ok(())
}
pub async fn get_package_links(&self) -> Vec<(String, Architecture, Link)> {
let mut components = Vec::new();
for architecture in &self.architectures {
for component in &self.components {
let link = match self.get_package_index_link(component, architecture).await {
Ok(link) => link,
Err(_) => {
info!("No link for component {component} and architecture {architecture}. Skipping.");
continue;
}
};
components.push((component.to_string(), architecture.clone(), link));
}
}
components
}
pub async fn get_package_index_link(
&self,
component: &str,
architecture: &Architecture,
) -> Result<Link> {
let index_url = if architecture == &Architecture::Source {
format!("{component}/source/Sources")
} else {
let arch_str = architecture.to_string();
format!("{component}/binary-{arch_str}/Packages")
};
let index_url = self.distro.url(&index_url, false);
let extensions = vec![".xz", ".gz", ""];
for ext in extensions {
let package_index = index_url.clone() + ext;
match self.links.get(&package_index) {
Some(link) => {
match get_etag(&link.url).await {
Ok(_) => return Ok(link.clone()), Err(_) => {
info!("No etag for {package_index}, trying next link.");
continue;
}
}
}
None => {
info!("Index {package_index} not found.");
}
}
}
Err(Error::new(
&format!("No matching package index found for component {component} and architecture {architecture}!"),
ErrorType::ApiUsage,
))
}
}
enum ReleaseSection {
Keywords,
HashMD5,
HashSHA1,
HashSHA256,
HashSHA512,
}
#[cfg(test)]
mod tests {
use crate::{Distro, Key, Release};
#[tokio::test]
async fn parse_ubuntu_jammy_release_file() {
let key = Key::key("/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg");
let distro = Distro::repo("http://archive.ubuntu.com/ubuntu", "jammy", key);
let release = Release::from_distro(&distro).await.unwrap();
assert_eq!(release.origin, Some("Ubuntu".to_string()), "Origin");
assert_eq!(release.label, Some("Ubuntu".to_string()), "Label");
assert_eq!(release.suite, Some("jammy".to_string()), "Suite");
assert_eq!(release.codename, Some("jammy".to_string()), "Codename");
assert_eq!(release.version, Some("22.04".to_string()), "Version");
assert_eq!(release.acquire_by_hash, true, "Acquire-By-Hash");
let release = Release::from_distro(&distro).await.unwrap();
release.check_compliance().unwrap();
}
#[tokio::test]
async fn parse_ebcl_release_file() {
let key = Key::armored_key("https://linux.elektrobit.com/eb-corbos-linux/ebcl_1.0_key.pub");
let distro = Distro::repo(
"http://linux.elektrobit.com/eb-corbos-linux/1.3",
"ebcl",
key,
);
let release = Release::from_distro(&distro).await.unwrap();
assert_eq!(release.origin, Some("Elektrobit".to_string()), "Origin");
assert_eq!(release.suite, Some("ebcl".to_string()), "Suite");
assert_eq!(release.codename, Some("ebcl".to_string()), "Codename");
let release = Release::from_distro(&distro).await.unwrap();
release.check_compliance().unwrap();
}
#[tokio::test]
async fn test_wrong_key() {
let key = Key::key("/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg");
let distro = Distro::repo(
"http://linux.elektrobit.com/eb-corbos-linux/1.3",
"ebcl",
key,
);
match Release::from_distro(&distro).await {
Ok(_) => assert!(false), Err(_) => {}
};
}
#[tokio::test]
async fn test_package_index_link() {
let key = Key::key("/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg");
let distro = Distro::repo("http://archive.ubuntu.com/ubuntu", "jammy", key);
let release = Release::from_distro(&distro).await.unwrap();
let link = release
.get_package_index_link("main", &crate::Architecture::Amd64)
.await
.unwrap();
assert_eq!(
link.url,
"http://archive.ubuntu.com/ubuntu/dists/jammy/main/binary-amd64/Packages.xz"
.to_string()
);
match release
.get_package_index_link("main", &crate::Architecture::Arm64)
.await
{
Ok(_) => assert!(false), Err(_) => {} };
}
#[tokio::test]
async fn test_get_package_links() {
let key = Key::key("/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg");
let distro = Distro::repo("http://archive.ubuntu.com/ubuntu", "jammy", key);
let release = Release::from_distro(&distro).await.unwrap();
let components = release.get_package_links().await;
println!("Components: {:?}", components);
println!("Found {} package indices.", components.len());
assert_eq!(components.len(), 8);
}
}