use std::fmt::Display;
use std::fmt::Formatter;
use std::ops::Deref;
use std::str::FromStr;
use std::time::Duration;
use std::time::SystemTime;
use chrono::DateTime;
use chrono::Utc;
use clap::builder::IntoResettable;
use clap::builder::OsStr;
use clap::builder::Resettable;
use typenum::Unsigned;
use crate::Result;
use crate::cli::KEY_ROTATE_RETIRE_IN_DURATION;
use crate::cli::THIRD_PARTY_CERTIFICATION_VALIDITY_DURATION;
use crate::cli::config;
use crate::cli::pki::vouch::CERTIFICATION_EXPIRATION;
use crate::cli::types::Time;
pub enum ExpirationKind {
Default,
Certification,
RetireIn,
}
impl From<usize> for ExpirationKind {
fn from(v: usize) -> ExpirationKind {
match v {
0 => {
debug_assert_eq!(0, DefaultKind::to_usize());
ExpirationKind::Default
},
1 => {
debug_assert_eq!(1, CertificationKind::to_usize());
ExpirationKind::Certification
},
2 => {
debug_assert_eq!(2, RetireInKind::to_usize());
ExpirationKind::RetireIn
},
_ => unreachable!(),
}
}
}
impl ExpirationKind {
fn name(&self) -> &'static str {
match self {
ExpirationKind::Default => "expiration",
ExpirationKind::Certification => "expiration",
ExpirationKind::RetireIn => "retire-in",
}
}
}
pub type DefaultKind = typenum::U0;
pub type CertificationKind = typenum::U1;
pub type RetireInKind = typenum::U2;
#[derive(Debug)]
pub struct ExpirationArg<Kind = DefaultKind> {
expiration: Expiration,
arguments: std::marker::PhantomData<Kind>,
}
impl<Kind> Deref for ExpirationArg<Kind> {
type Target = Expiration;
fn deref(&self) -> &Self::Target {
&self.expiration
}
}
impl<Kind> ExpirationArg<Kind> {
pub fn value(&self) -> Expiration {
self.expiration.clone()
}
}
impl<Kind> clap::Args for ExpirationArg<Kind>
where
Kind: typenum::Unsigned,
{
fn augment_args(cmd: clap::Command) -> clap::Command {
let kind: ExpirationKind = Kind::to_usize().into();
const EXPIRATION_LONG_HELP: &str = "\
Sets the expiration time
EXPIRATION is either an ISO 8601 formatted date with an optional time \
or a custom duration. A duration takes the form `N[ymwds]`, where the \
letters stand for years, months, weeks, days, and seconds, respectively. \
Alternatively, the keyword `never` does not set an expiration time.";
const RETIRE_IN_LONG_HELP: &str = "\
Sets the time at which the certificate should be retired
TIME is either an ISO 8601 formatted date with an optional time \
or a custom duration. A duration takes the form `N[ymwds]`, where the \
letters stand for years, months, weeks, days, and seconds, respectively. \
Alternatively, the keyword `never` skips the certification of a \
revocation certificate.";
let name = kind.name();
cmd.arg(
clap::Arg::new(name)
.long(name)
.allow_hyphen_values(true)
.value_name(match kind {
ExpirationKind::Default => "EXPIRATION",
ExpirationKind::Certification => "EXPIRATION",
ExpirationKind::RetireIn => "TIME",
})
.value_parser(Expiration::new)
.default_value(match kind {
ExpirationKind::Default => Expiration::Never,
ExpirationKind::Certification =>
Expiration::from_duration(
THIRD_PARTY_CERTIFICATION_VALIDITY_DURATION),
ExpirationKind::RetireIn =>
Expiration::from_duration(
KEY_ROTATE_RETIRE_IN_DURATION),
})
.help(match kind {
ExpirationKind::Default | ExpirationKind::Certification =>
"Sets the expiration time",
ExpirationKind::RetireIn =>
"Sets the time at which the certificate should be retired",
})
.long_help(match kind {
ExpirationKind::Default => EXPIRATION_LONG_HELP.into(),
ExpirationKind::Certification =>
config::augment_help(CERTIFICATION_EXPIRATION,
EXPIRATION_LONG_HELP),
ExpirationKind::RetireIn => RETIRE_IN_LONG_HELP.into(),
})
)
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
}
impl<Kind> clap::FromArgMatches for ExpirationArg<Kind>
where
Kind: typenum::Unsigned
{
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches)
-> clap::error::Result<()>
{
let kind: ExpirationKind = Kind::to_usize().into();
if let Some(v) = matches.get_one::<Expiration>(kind.name()) {
self.expiration = v.clone();
}
Ok(())
}
fn from_arg_matches(matches: &clap::ArgMatches)
-> clap::error::Result<Self>
{
let mut expiration = ExpirationArg {
expiration: Expiration::Never,
arguments: Default::default(),
};
expiration.update_from_arg_matches(matches)?;
Ok(expiration)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum Expiration {
Timestamp(Time),
Never,
}
impl Expiration {
pub fn new(expiry: &str) -> Result<Self> {
match expiry {
"never" => Ok(Expiration::Never),
_ => Ok(Expiration::Timestamp(Time::from_str(expiry)?)),
}
}
pub fn from_duration(duration: Duration) -> Self {
Expiration::Timestamp(Time::from_duration(duration))
}
pub fn as_duration(
&self,
reference: DateTime<Utc>,
) -> Result<Option<Duration>> {
match self {
Expiration::Timestamp(time) => {
Ok(Some(time.duration_since(reference.into())?))
}
Expiration::Never => Ok(None),
}
}
pub fn to_system_time(&self, now: SystemTime) -> Result<Option<SystemTime>> {
match self {
Expiration::Timestamp(t) => Ok(Some(t.to_system_time(now)?)),
Expiration::Never => Ok(None),
}
}
}
impl FromStr for Expiration {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Expiration> {
Expiration::new(s)
}
}
impl Display for Expiration {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
Expiration::Timestamp(time) => write!(f, "{}", time),
Expiration::Never => write!(f, "never"),
}
}
}
impl IntoResettable<OsStr> for Expiration {
fn into_resettable(self) -> Resettable<OsStr> {
Resettable::Value(format!("{}", self).into()).into()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::SECONDS_IN_YEAR;
#[test]
fn test_expiry() {
assert_eq!(
Expiration::new("1y").unwrap(),
Expiration::from_duration(Duration::new(SECONDS_IN_YEAR, 0)),
);
assert_eq!(
Expiration::new("2023-05-15T20:00:00Z").unwrap(),
Expiration::Timestamp(Time::from_str("2023-05-15T20:00:00Z").unwrap()),
);
assert_eq!(
Expiration::new("never").unwrap(),
Expiration::Never,
);
}
#[test]
fn test_expiry_as_duration() {
let reference = DateTime::from_timestamp(1, 0).unwrap();
let expiry = Expiration::Timestamp(
Time::try_from(DateTime::from_timestamp(2, 0).unwrap())
.expect("valid"));
assert_eq!(
expiry.as_duration(reference).unwrap(),
Some(Duration::new(1, 0)),
);
let expiry = Expiration::from_duration(Duration::new(2, 0));
assert_eq!(
expiry.as_duration(reference).unwrap(),
Some(Duration::new(2, 0)),
);
let expiry = Expiration::Never;
assert_eq!(expiry.as_duration(reference).unwrap(), None);
}
#[test]
fn test_expiry_as_duration_errors() {
let reference = DateTime::from_timestamp(2, 0).unwrap();
let expiry = Expiration::Timestamp(
Time::try_from(DateTime::from_timestamp(1, 0).unwrap())
.expect("valid"));
assert!(expiry.as_duration(reference).is_err());
}
}