use super::*;
use serde::{Deserialize, Deserializer, de::Error};
use std::sync::LazyLock;
pub(super) fn expiration<'de, D: Deserializer<'de>>(
input: D,
) -> Result<Option<JwtExpiration>, D::Error> {
Option::<Value>::deserialize(input)?
.map(|value| match value {
Value::Number(value) => Ok(JwtExpiration::At(value)),
Value::String(value) => relative_seconds(&value)
.and_then(|seconds| {
if !seconds.is_finite()
|| !(i64::MIN as f64..i64::MAX as f64).contains(&seconds)
{
return Err("JWT lifetime is out of range".into());
}
Duration::try_seconds(seconds as i64)
.ok_or_else(|| "JWT lifetime is out of range".into())
})
.map(JwtExpiration::After)
.map_err(D::Error::custom),
_ => Err(D::Error::custom(
"JWT expirationTime must be a number or time string",
)),
})
.transpose()
}
pub(super) fn duration<'de, D: Deserializer<'de>>(input: D) -> Result<Option<Duration>, D::Error> {
Option::<f64>::deserialize(input)?
.map(|seconds| {
if !seconds.is_finite() || !(i64::MIN as f64..i64::MAX as f64).contains(&seconds) {
return Err(D::Error::custom("JWT rotationInterval is out of range"));
}
Duration::try_seconds(seconds.trunc() as i64)
.and_then(|whole| {
whole.checked_add(&Duration::nanoseconds((seconds.fract() * 1e9) as i64))
})
.ok_or_else(|| D::Error::custom("JWT rotationInterval is out of range"))
})
.transpose()
}
pub(in crate::plugins::jwt) fn relative_seconds(value: &str) -> Result<f64, String> {
static PERIOD: LazyLock<Result<regex::Regex, regex::Error>> = LazyLock::new(|| {
regex::Regex::new(
r"(?i)^(\+|\-)? ?([0-9]+|[0-9]+\.[0-9]+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|months?|mo|years?|yrs?|y)(?: (ago|from now))?(?:\r\n|[\n\r\u{2028}\u{2029}])?$",
)
});
let invalid = || {
format!(
"Invalid time string format: \"{value}\". Use formats like \"7d\", \"30m\", \"1 hour\", etc."
)
};
let captures = PERIOD
.as_ref()
.map_err(ToString::to_string)?
.captures(value)
.ok_or_else(invalid)?;
if captures.get(1).is_some() && captures.get(4).is_some() {
return Err(invalid());
}
let amount: f64 = captures
.get(2)
.ok_or_else(invalid)?
.as_str()
.parse()
.map_err(|_| invalid())?;
let unit = captures
.get(3)
.ok_or_else(invalid)?
.as_str()
.to_ascii_lowercase();
let scale = match unit.as_str() {
"year" | "years" | "yr" | "yrs" | "y" => 31557600.0,
"month" | "months" | "mo" => 2592000.0,
"week" | "weeks" | "w" => 604800.0,
"day" | "days" | "d" => 86400.0,
"hour" | "hours" | "hr" | "hrs" | "h" => 3600.0,
"minute" | "minutes" | "min" | "mins" | "m" => 60.0,
"second" | "seconds" | "sec" | "secs" | "s" => 1.0,
_ => return Err(invalid()),
};
let negative = captures.get(1).is_some_and(|value| value.as_str() == "-")
|| captures.get(4).is_some_and(|value| value.as_str() == "ago");
let milliseconds = amount * (scale * 1_000.0);
let seconds = if negative {
-milliseconds / 1_000.0
} else {
milliseconds / 1_000.0
};
Ok(if (-0.5..0.0).contains(&seconds) {
-0.0
} else {
let integral = seconds.floor();
if seconds - integral < 0.5 {
integral
} else {
integral + 1.0
}
})
}