use {
crate::{
control::{MutableControlFile, MutableControlStanza},
hash::Hash,
indexfile::IndexFile,
release::Release,
},
chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc},
clap::Args,
futures::AsyncReadExt,
serde::{Deserialize, Serialize},
smol::{fs, io},
std::path::{Path, PathBuf},
};
pub const DEBIAN_KEYRING: &[u8] = include_bytes!("../keyring/keys.bin");
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub enum SignedBy {
#[default]
Builtin,
Key(String),
Keyring(PathBuf),
}
impl SignedBy {
pub const MAX_KEY_SIZE: usize = 64 * 1024; async fn import_into(&self, ctx: &mut gpgme::Context, base_dir: &Path) -> io::Result<()> {
match self {
Self::Builtin => {
ctx.import(DEBIAN_KEYRING)?;
}
Self::Key(key) => {
ctx.import(key.as_bytes())?;
}
Self::Keyring(path) => {
let path = resolve_path(base_dir, path);
let size = fs::metadata(&path)
.await
.and_then(|md| {
if md.is_file() {
let size = md.len();
if size <= Self::MAX_KEY_SIZE as u64 {
Ok(size)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"the key file is too large",
))
}
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"the key is not a regular file",
))
}
})
.map_err(|err| {
io::Error::other(format!(
"failed to read key file {}: {}",
path.display(),
err
))
})?;
let mut buf = Vec::with_capacity(size as usize);
fs::File::open(&path).await?.read_to_end(&mut buf).await?;
ctx.import(buf)?;
}
}
Ok(())
}
}
fn resolve_path(base_dir: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
base_dir.join(path)
}
}
impl From<&Path> for SignedBy {
fn from(p: &Path) -> Self {
SignedBy::Keyring(p.to_path_buf())
}
}
impl From<&PathBuf> for SignedBy {
fn from(p: &PathBuf) -> Self {
Self::from(p.as_path())
}
}
impl serde::ser::Serialize for SignedBy {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
SignedBy::Builtin => serializer.serialize_none(),
SignedBy::Key(s) => serializer.serialize_str(s),
SignedBy::Keyring(s) => serializer.serialize_str(&s.to_string_lossy()),
}
}
}
impl From<&SignedBy> for String {
fn from(s: &SignedBy) -> Self {
match s {
SignedBy::Builtin => "builtin".to_string(),
SignedBy::Key(s) => s.clone(),
SignedBy::Keyring(s) => s.to_string_lossy().into_owned(),
}
}
}
impl<'de> serde::de::Deserialize<'de> for SignedBy {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct SignedByVisitor;
impl serde::de::Visitor<'_> for SignedByVisitor {
type Value = SignedBy;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"a string containing either a PGP public key block or a path to keyring file"
)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
let s = v.trim().to_string();
if s.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----") {
Ok(SignedBy::Key(s))
} else {
Ok(SignedBy::Keyring(s.into()))
}
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
let v = v.trim();
if v.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----") {
Ok(SignedBy::Key(v.to_string()))
} else {
Ok(SignedBy::Keyring(v.into()))
}
}
}
deserializer.deserialize_string(SignedByVisitor)
}
}
#[derive(Clone)]
pub(crate) struct ClapSignedByParser;
impl clap::builder::TypedValueParser for ClapSignedByParser {
type Value = SignedBy;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
if let [b'@', rest @ ..] = value.as_encoded_bytes() {
let path = Path::new(unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(rest) });
let key_data = std::fs::read_to_string(path).map_err(|e| {
let mut err =
clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string_lossy().into()),
);
err.insert(
clap::error::ContextKind::Custom,
clap::error::ContextValue::String(e.to_string()),
);
err
})?;
let trimmed = key_data.trim();
const HDR: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----";
const FTR: &str = "-----END PGP PUBLIC KEY BLOCK-----";
if trimmed.starts_with(HDR) && trimmed.ends_with(FTR) {
Ok(SignedBy::Key(key_data))
} else {
let mut err =
clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string_lossy().into()),
);
err.insert(
clap::error::ContextKind::Custom,
clap::error::ContextValue::String(
"not a valid ASCII-armored OpenPGP public key block".into(),
),
);
Err(err)
}
} else {
Ok(SignedBy::Keyring(Path::new(value).into()))
}
}
}
#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
pub struct SnapshotId(pub DateTime<Utc>);
impl SnapshotId {
pub fn format(&self, fmt: &str) -> String {
self.0.format(fmt).to_string()
}
}
impl From<&DateTime<Utc>> for SnapshotId {
fn from(dt: &DateTime<Utc>) -> Self {
SnapshotId(*dt)
}
}
impl From<&DateTime<Utc>> for Snapshot {
fn from(dt: &DateTime<Utc>) -> Self {
Snapshot::Use(dt.into())
}
}
impl std::fmt::Display for SnapshotId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.format("%Y%m%dT%H%M%SZ"))
}
}
impl From<SnapshotId> for Snapshot {
fn from(dt: SnapshotId) -> Self {
Snapshot::Use(dt)
}
}
macro_rules! parse_snapshot {
($var:expr, [ $($fmt:literal),+ $(,)? ]) => {
$(if let Some(parsed) = SnapshotFormatSpec::new($fmt).parse($var) {
Ok(SnapshotId(parsed))
} else
)+
{
Err(format!("invalid snapshot ID '{}' - expected a timestamp", $var))
}
}
}
impl TryFrom<&str> for SnapshotId {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
parse_snapshot!(
s.trim(),
[
"%Y%m%dT%H%M%SZ",
"%Y%m%dT%H%M%S%z",
"%Y-%m-%d",
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%dT%H:%M:%S",
"%Y%m%d",
"%Y%m%dT%H%M%S",
]
)
}
}
#[derive(Clone)]
pub struct SnapshotIdArgParser;
impl clap::builder::TypedValueParser for SnapshotIdArgParser {
type Value = SnapshotId;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let value = value.to_str().ok_or_else(|| {
let mut err = clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err
})?;
if value.eq_ignore_ascii_case("now") {
Ok(SnapshotId(Utc::now()))
} else if value.eq_ignore_ascii_case("today") {
let now = Utc::now();
let today = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
Ok(SnapshotId(DateTime::<Utc>::from_naive_utc_and_offset(
today, Utc,
)))
} else {
value.try_into().map_err(|e: String| {
let mut err =
clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string()),
);
err.insert(
clap::error::ContextKind::Custom,
clap::error::ContextValue::String(e),
);
err
})
}
}
}
#[derive(Debug, Serialize, Clone, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields)]
pub enum Snapshot {
#[default]
Disable,
Enable,
Use(SnapshotId),
}
impl std::fmt::Display for Snapshot {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Snapshot::Disable => write!(f, "disable"),
Snapshot::Enable => write!(f, "enable"),
Snapshot::Use(s) => write!(f, "{}", s.format("%Y%m%dT%H%M%SZ")),
}
}
}
impl From<&Snapshot> for String {
fn from(s: &Snapshot) -> Self {
format!("{}", s)
}
}
struct SnapshotFormatSpec {
pub fmt: &'static str,
pub with_tz: bool,
}
impl SnapshotFormatSpec {
pub const fn new(fmt: &'static str) -> Self {
const fn contains_tz(fmt: &str) -> bool {
let mut b = fmt.as_bytes();
loop {
b = match b {
[] => return false,
[b'%', b'Z', ..] => return true,
[b'%', b'z', ..] => return true,
[b'%', b':', b'z', ..] => return true,
[b'%', b':', b':', b'z', ..] => return true,
[b'%', b':', b':', b':', b'z', ..] => return true,
[b'%', b'#', b'z', ..] => return true,
[_, rest @ ..] => rest,
}
}
}
Self {
fmt,
with_tz: contains_tz(fmt),
}
}
fn parse(&self, s: &str) -> Option<DateTime<Utc>> {
if self.with_tz {
DateTime::<FixedOffset>::parse_from_str(s, self.fmt)
.ok()
.map(|dt| dt.with_timezone(&Utc))
} else {
NaiveDateTime::parse_from_str(s, self.fmt)
.ok()
.and_then(|ndt| ndt.and_local_timezone(Local).single())
.map(|dt| dt.with_timezone(&Utc))
}
}
}
impl TryFrom<&str> for Snapshot {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let s = s.trim();
if s.eq_ignore_ascii_case("disable") {
Ok(Snapshot::Disable)
} else if s.eq_ignore_ascii_case("enable") {
Ok(Snapshot::Enable)
} else {
TryFrom::<&str>::try_from(s).map(Snapshot::Use)
}
}
}
impl<'de> serde::de::Deserialize<'de> for Snapshot {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct SnapshotVisitor;
impl serde::de::Visitor<'_> for SnapshotVisitor {
type Value = Snapshot;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"a \"no\" if snapshot use is disabled or snapshot ID to use"
)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.trim().try_into().map_err(|e: String| E::custom(e))
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
v.trim().try_into().map_err(|e: String| E::custom(e))
}
}
deserializer.deserialize_string(SnapshotVisitor)
}
}
pub(crate) enum RepoPath<'a> {
Flat(&'a str),
Structured { suite: &'a str },
}
impl<'a> RepoPath<'a> {
pub(crate) fn file(&self, f: &str) -> String {
match self {
RepoPath::Flat("") => f.to_string(),
RepoPath::Flat(suite) => format!("{}/{}", suite, f),
RepoPath::Structured { suite } => format!("dists/{}/{}", suite, f),
}
}
}
#[derive(Clone)]
pub(crate) struct ClapSnapshotParser;
impl clap::builder::TypedValueParser for ClapSnapshotParser {
type Value = Snapshot;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let value = value.to_str().ok_or_else(|| {
let mut err = clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err
})?;
value.try_into().map_err(|e: String| {
let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string()),
);
err.insert(
clap::error::ContextKind::Custom,
clap::error::ContextValue::String(e),
);
err
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ArchiveHashKind {
#[default]
SHA256,
SHA512,
MD5sum,
}
impl ArchiveHashKind {
pub fn is_sha256(&self) -> bool {
*self == ArchiveHashKind::SHA256
}
pub fn name(&self) -> &'static str {
match self {
ArchiveHashKind::MD5sum => "MD5",
ArchiveHashKind::SHA256 => "SHA256",
ArchiveHashKind::SHA512 => "SHA512",
}
}
}
impl std::str::FromStr for ArchiveHashKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"md5" | "md5sum" => Ok(ArchiveHashKind::MD5sum),
"sha256" => Ok(ArchiveHashKind::SHA256),
"sha512" => Ok(ArchiveHashKind::SHA512),
other => Err(format!("unsupported hash: {other}")),
}
}
}
#[derive(Clone)]
struct ArchiveHashKindValueParser;
impl clap::builder::TypedValueParser for ArchiveHashKindValueParser {
type Value = ArchiveHashKind;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let val: ArchiveHashKind = value
.to_str()
.ok_or_else(|| {
let mut err = clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err
})?
.parse()
.map_err(|e: String| {
let mut err =
clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(value.to_string_lossy().into()),
);
err.insert(
clap::error::ContextKind::Custom,
clap::error::ContextValue::String(e),
);
err
})?;
Ok(val)
}
}
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub struct RepositoryFile {
pub(crate) path: String,
#[serde(skip)]
pub(crate) fetch_path: Option<String>,
pub(crate) hash: Hash,
pub(crate) size: u64,
}
impl RepositoryFile {
pub fn new(path: String, hash: Hash, size: u64) -> Self {
Self {
path,
fetch_path: None,
hash,
size,
}
}
pub fn path(&self) -> &str {
&self.path
}
pub fn fetch_path(&self) -> &str {
self.fetch_path.as_deref().unwrap_or(&self.path)
}
pub fn size(&self) -> u64 {
self.size
}
pub fn hash(&self) -> &Hash {
&self.hash
}
}
pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &T::default()
}
#[derive(Debug, Args, Clone, PartialEq, Eq, Default)]
pub(crate) struct ArchiveOptionsArgs {
#[arg(long = "only-arch", value_name = "ARCH", value_delimiter = ',')]
pub arch: Vec<String>,
#[arg(short = 'K', long = "allow-insecure", action)]
pub allow_insecure: bool,
#[arg(
long = "signed-by",
value_name = "@INLINE-KEY|KEYRING",
value_parser = ClapSignedByParser
)]
pub signed_by: Option<SignedBy>,
#[arg(long = "snapshots", value_name = "URL")]
pub snapshots: Option<String>,
#[arg(long = "snapshot", value_name = "SNAPSHOT", value_parser = ClapSnapshotParser)]
pub snapshot: Option<Snapshot>,
#[arg(
short = 's',
long = "suite",
value_name = "SUITE",
value_delimiter = ','
)]
pub suites: Vec<String>,
#[arg(
short = 'C',
long = "components",
value_delimiter = ',',
value_name = "COMPONENT"
)]
pub components: Vec<String>,
#[arg(
hide = true,
long = "hash",
value_parser = ArchiveHashKindValueParser,
value_name = "md5|sha1|sha256|sha512",
default_value = "sha256",
)]
pub hash: ArchiveHashKind,
#[arg(long = "priority", value_name = "N")]
pub priority: Option<u32>,
}
#[derive(Debug, Args, Clone, PartialEq, Eq, Default)]
pub(crate) struct ArchiveArgs {
#[arg(value_name = "URL")]
pub url: String,
#[command(flatten)]
pub options: ArchiveOptionsArgs,
}
#[derive(Debug, Args, Clone, PartialEq, Eq, Default)]
pub(crate) struct OptionalArchiveArgs {
#[arg(value_name = "URL", required_unless_present = "import")]
pub url: Option<String>,
#[command(flatten)]
pub options: ArchiveOptionsArgs,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields)]
pub struct Archive {
pub url: String,
#[serde(skip)]
real_url: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub arch: Vec<String>,
#[serde(default, rename = "allow-insecure", skip_serializing_if = "is_default")]
pub allow_insecure: bool,
#[serde(default, rename = "signed-by", skip_serializing_if = "Option::is_none")]
pub signed_by: Option<SignedBy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshots: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot: Option<Snapshot>,
pub suites: Vec<String>,
#[serde(alias = "comp", default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<String>,
#[serde(default, skip_serializing_if = "ArchiveHashKind::is_sha256")]
pub hash: ArchiveHashKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<u32>,
}
impl From<ArchiveArgs> for Archive {
fn from(value: ArchiveArgs) -> Self {
Archive {
url: value.url,
arch: value.options.arch,
allow_insecure: value.options.allow_insecure,
signed_by: value.options.signed_by,
snapshots: value.options.snapshots,
snapshot: value.options.snapshot,
suites: value.options.suites,
components: value.options.components,
hash: value.options.hash,
priority: value.options.priority,
..Default::default()
}
}
}
impl From<&ArchiveArgs> for Archive {
fn from(value: &ArchiveArgs) -> Self {
Archive {
url: value.url.clone(),
arch: value.options.arch.clone(),
allow_insecure: value.options.allow_insecure,
signed_by: value.options.signed_by.clone(),
snapshots: value.options.snapshots.clone(),
snapshot: value.options.snapshot.clone(),
suites: value.options.suites.clone(),
components: value.options.components.clone(),
hash: value.options.hash,
priority: value.options.priority,
..Default::default()
}
}
}
impl OptionalArchiveArgs {
pub(crate) fn to_archive(&self) -> Option<Archive> {
self.url.as_ref().map(|url| Archive {
url: url.clone(),
arch: self.options.arch.clone(),
allow_insecure: self.options.allow_insecure,
signed_by: self.options.signed_by.clone(),
snapshots: self.options.snapshots.clone(),
snapshot: self.options.snapshot.clone(),
suites: self.options.suites.clone(),
components: self.options.components.clone(),
hash: self.options.hash,
priority: self.options.priority,
..Default::default()
})
}
}
impl Archive {
pub fn should_include_arch(&self, arch: &str) -> bool {
self.arch.is_empty() || self.arch.iter().any(|s| s == arch)
}
pub fn allow_insecure(&self) -> bool {
self.allow_insecure
}
pub fn with_snapshots<S: AsRef<str>>(mut self, snapshots: S) -> Self {
self.snapshots = Some(snapshots.as_ref().to_string());
self
}
fn is_flat_repo(suite: &str) -> bool {
suite.ends_with('/')
}
pub fn is_flat(&self) -> bool {
!self.suites.is_empty() && self.suites.iter().all(|s| Self::is_flat_repo(s))
}
pub(crate) fn suite_path_prefix(suite: &str) -> RepoPath<'_> {
if Self::is_flat_repo(suite) {
RepoPath::Flat(
suite
.strip_prefix("./")
.unwrap_or(suite)
.trim_end_matches('/'),
)
} else {
RepoPath::Structured { suite }
}
}
pub(crate) fn release_path(&self, suite: &str) -> String {
let name = if self.allow_insecure() {
"Release"
} else {
"InRelease"
};
Self::suite_path_prefix(suite).file(name)
}
pub fn validate(&self) -> io::Result<()> {
if self.suites.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"archive: `suites` must not be empty; \
use `suites = [\"/\"]` for a flat (no dists/ hierarchy) repository",
));
}
let flat_count = self.suites.iter().filter(|s| Self::is_flat_repo(s)).count();
if flat_count != 0 && flat_count != self.suites.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"archive: suites must not mix flat entries (ending '/') with non-flat entries",
));
}
if flat_count > 0 && !self.components.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"archive: flat repository suites cannot set `components`",
));
}
if flat_count == 0 && self.components.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"archive: non-flat repository requires at least one component",
));
}
Ok(())
}
pub(crate) async fn release_from_file(
&self,
r: IndexFile,
skip_verify: bool,
base_dir: &Path,
) -> io::Result<Release> {
if self.allow_insecure() {
Release::new(r).map_err(std::io::Error::other)
} else if skip_verify {
Release::new(r.clear_text()).map_err(std::io::Error::other)
} else {
self.verify_signed_release(&r, base_dir).await
}
}
async fn verify_signed_release<T>(&self, data: T, base_dir: &Path) -> io::Result<Release>
where
T: AsRef<str>,
{
let mut ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?;
let tempdir = tempfile::tempdir()?;
ctx.set_engine_home_dir(tempdir.path().as_os_str().as_encoded_bytes())?;
ctx.set_flag("auto-key-retrieve", "0")?;
self.signed_by
.as_ref()
.unwrap_or(&SignedBy::Builtin)
.import_into(&mut ctx, base_dir)
.await?;
let mut plaintext = Vec::new();
let verify_result = ctx.verify_opaque(data.as_ref().as_bytes(), &mut plaintext)?;
if let Some(signature) = verify_result.signatures().next() {
if let Err(err) = signature.status() {
return Err(err.into());
}
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"no signature found in InRelease",
));
}
Release::try_from(plaintext).map_err(std::io::Error::other)
}
pub(crate) fn set_base(&mut self) {
if let Some(snapshots_template) = &self.snapshots {
if let Some(Snapshot::Use(snap)) = &self.snapshot {
self.real_url = Some(
snapshots_template
.trim_end_matches('/')
.replace("@SNAPSHOTID@", &snap.format("%Y%m%dT%H%M%SZ").to_string()),
);
}
}
}
pub(crate) fn base(&self) -> &str {
self.real_url.as_deref().unwrap_or(&self.url)
}
pub(crate) fn apt_uri(&self) -> String {
if let Some(snapshots_template) = &self.snapshots {
if let Some(Snapshot::Use(snap)) = &self.snapshot {
return snapshots_template
.trim_end_matches('/')
.replace("@SNAPSHOTID@", &snap.format("%Y%m%dT%H%M%SZ").to_string());
}
}
self.url.trim_end_matches('/').to_string()
}
pub fn file_url<P: AsRef<str>>(&self, path: P) -> String {
if let Some(snapshots_template) = &self.snapshots {
if let Some(Snapshot::Use(snap)) = &self.snapshot {
return format!(
"{}/{}",
snapshots_template
.trim_end_matches('/')
.replace("@SNAPSHOTID@", &snap.format("%Y%m%dT%H%M%SZ").to_string()),
path.as_ref()
);
}
}
format!("{}/{}", self.url.trim_end_matches('/'), path.as_ref())
}
pub fn as_vendor(&self) -> Option<(Vec<Self>, Vec<String>)> {
match self.url.to_ascii_lowercase().as_str() {
"debian" => {
let mut archive = self.clone();
let mut security: Option<Archive> = None;
archive.url = "https://ftp.debian.org/debian/".to_string();
if self.snapshot.is_none() {
archive.snapshots = Some(
"https://snapshot.debian.org/archive/debian/@SNAPSHOTID@/".to_string(),
);
}
if self.components.is_empty() {
archive.components = vec!["main".to_string()];
}
if !self.suites.is_empty() && self.suites.len() < 2 {
let s = self.suites[0].as_str();
if s != "sid" && s != "unstable" {
archive.suites = ["", "-updates", "-backports"]
.iter()
.map(|f| format!("{}{}", s, f))
.collect();
security = Some(Archive {
url: "https://security.debian.org/debian-security/".to_string(),
suites: vec![format!("{}-security", s)],
snapshots: Some(
"https://snapshot.debian.org/archive/debian-security/@SNAPSHOTID@/"
.to_string(),
),
..archive.clone()
});
}
}
let mut archives = vec![archive];
archives.extend(security);
Some((archives, vec!["debian-keyring".to_string()]))
}
"ubuntu" => {
let mut archive = self.clone();
archive.url = "https://archive.ubuntu.com/ubuntu/".to_string();
if self.snapshot.is_none() {
archive.snapshots =
Some("https://snapshot.ubuntu.com/ubuntu/@SNAPSHOTID@/".to_string());
}
if self.components.is_empty() {
archive.components = vec!["main".to_string(), "universe".to_string()];
}
if !self.suites.is_empty() && self.suites.len() < 2 {
let s = self.suites[0].as_str();
archive.suites = ["", "-updates", "-backports", "-security"]
.iter()
.map(|f| format!("{}{}", s, f))
.collect();
}
Some((vec![archive], vec!["ubuntu-keyring".to_string()]))
}
"devuan" => {
let mut archive = self.clone();
archive.url = "http://deb.devuan.org/merged/".to_string();
archive.snapshots = None;
if self.components.is_empty() {
archive.components = vec!["main".to_string()];
}
if !self.suites.is_empty() && self.suites.len() < 2 {
let s = self.suites[0].as_str();
if s != "ceres"
&& s != "unstable"
&& !s.chars().next().is_some_and(|c| c.is_ascii_digit())
{
archive.suites = ["", "-updates", "-backports", "-security"]
.iter()
.map(|f| format!("{}{}", s, f))
.collect();
}
}
Some((vec![archive], vec!["devuan-keyring".to_string()]))
}
_ => None,
}
}
}
impl From<&Archive> for MutableControlFile {
fn from(src: &Archive) -> Self {
let make_base = || {
let mut cs = MutableControlStanza::parse("Types: deb\n").unwrap();
cs.set("URIs", src.apt_uri());
if !src.arch.is_empty() {
cs.set("Architectures", src.arch.join(" "));
}
if src.allow_insecure {
cs.set("Allow-Insecure", "yes");
}
if let Some(signed_by) = &src.signed_by {
cs.set("Signed-By", String::from(signed_by));
}
cs
};
let mut file = MutableControlFile::new();
if src.is_flat() {
for suite in &src.suites {
let mut cs = make_base();
cs.set("Suites", suite.clone());
file.add(cs);
}
} else {
let mut cs = make_base();
cs.set("Suites", src.suites.join(" "));
cs.set("Components", src.components.join(" "));
file.add(cs);
}
file
}
}