use core::cmp::Ordering;
use crate::error::{Error, Result};
use crate::json::{MAX_SAFE_INTEGER, Number, Value};
const MAX_SAFE_F64: f64 = MAX_SAFE_INTEGER as f64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NumericDate(Number);
impl NumericDate {
pub fn from_json(value: &Value, claim: &'static str) -> Result<Self> {
match value {
Value::Number(n) => Self::from_number(*n).ok_or(Error::InvalidClaim(claim)),
_ => Err(Error::InvalidClaim(claim)),
}
}
pub fn from_secs(secs: i64) -> Option<Self> {
Number::from_i64(secs).map(Self)
}
pub fn from_f64(value: f64) -> Option<Self> {
Number::from_f64(value).and_then(Self::from_number)
}
pub fn as_f64(&self) -> f64 {
self.0.as_f64()
}
pub fn as_secs(&self) -> Option<i64> {
self.0.as_i64()
}
pub fn to_json_value(&self) -> Value {
Value::Number(self.0)
}
pub fn is_expired(&self, now: i64, leeway: u32) -> bool {
self.cmp_secs(now.saturating_sub(i64::from(leeway))) != Ordering::Greater
}
pub fn is_not_yet_valid(&self, now: i64, leeway: u32) -> bool {
self.cmp_secs(now.saturating_add(i64::from(leeway))) == Ordering::Greater
}
pub(crate) fn cmp_secs(&self, t: i64) -> Ordering {
let value = self.0.as_f64();
let floor = value.floor();
match (floor as i64).cmp(&t) {
Ordering::Equal if value != floor => Ordering::Greater,
ordering => ordering,
}
}
fn from_number(n: Number) -> Option<Self> {
(n.as_f64().abs() <= MAX_SAFE_F64).then_some(Self(n))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::parse;
fn date(json: &str) -> Result<NumericDate> {
NumericDate::from_json(&parse(json.as_bytes())?, "exp")
}
fn at(json: &str) -> NumericDate {
date(json).unwrap()
}
#[test]
fn accepts_json_numbers_within_the_safe_range() {
for (text, value) in [
("1300819380", 1_300_819_380.0),
("1300819380.5", 1_300_819_380.5),
("-1", -1.0),
("0", 0.0),
("9007199254740991", 9_007_199_254_740_991.0),
("-9007199254740991", -9_007_199_254_740_991.0),
] {
assert_eq!(at(text).as_f64(), value, "{text}");
}
}
#[test]
fn rejects_out_of_range_and_non_numbers_with_the_claim_name() {
for text in [
"9007199254740992",
"1e300",
"-9007199254740992",
r#""1300819380""#,
"true",
"null",
"[]",
"{}",
] {
assert_eq!(date(text), Err(Error::InvalidClaim("exp")), "{text}");
}
let nbf = NumericDate::from_json(&Value::from("1"), "nbf");
assert_eq!(nbf, Err(Error::InvalidClaim("nbf")));
}
#[test]
fn fractions_are_kept_exactly_and_integers_serialize_without_one() {
assert_eq!(at("1300819380.5").to_json_value().to_json(), "1300819380.5");
assert_eq!(at("1e3").to_json_value().to_json(), "1000");
assert_eq!(at("1e3").as_secs(), Some(1000));
assert_eq!(at("1300819380.5").as_secs(), None);
assert_eq!(at("-1").to_json_value().to_json(), "-1");
let v = at("1300819380.25");
assert_eq!(NumericDate::from_json(&v.to_json_value(), "iat"), Ok(v));
}
#[test]
fn constructors_enforce_the_same_range() {
assert_eq!(
NumericDate::from_secs(1000).map(|d| d.as_f64()),
Some(1000.0)
);
assert_eq!(
NumericDate::from_secs(MAX_SAFE_INTEGER).and_then(|d| d.as_secs()),
Some(MAX_SAFE_INTEGER)
);
assert_eq!(NumericDate::from_secs(MAX_SAFE_INTEGER + 1), None);
assert_eq!(NumericDate::from_secs(-MAX_SAFE_INTEGER - 1), None);
assert_eq!(NumericDate::from_secs(i64::MIN), None);
assert_eq!(NumericDate::from_f64(0.5).map(|d| d.as_f64()), Some(0.5));
assert_eq!(NumericDate::from_f64(9_007_199_254_740_992.0), None);
assert_eq!(NumericDate::from_f64(f64::NAN), None);
assert_eq!(NumericDate::from_f64(f64::INFINITY), None);
assert_eq!(
NumericDate::from_f64(-0.0).and_then(|d| d.as_secs()),
Some(0)
);
}
#[test]
fn exp_boundaries_of_adr_0027_section_3() {
let exp = at("1000");
assert!(!exp.is_expired(1059, 60));
assert!(exp.is_expired(1060, 60));
assert!(!exp.is_expired(999, 0));
assert!(exp.is_expired(1000, 0));
let half = at("1000.5");
assert!(!half.is_expired(1000, 0));
assert!(half.is_expired(1001, 0));
assert!(at("-1").is_expired(0, 0));
}
#[test]
fn nbf_boundaries_of_adr_0027_section_3() {
let nbf = at("1000");
assert!(!nbf.is_not_yet_valid(940, 60));
assert!(nbf.is_not_yet_valid(939, 60));
assert!(!nbf.is_not_yet_valid(1000, 0));
assert!(nbf.is_not_yet_valid(999, 0));
let half = at("1000.5");
assert!(half.is_not_yet_valid(1000, 0));
assert!(!half.is_not_yet_valid(1001, 0));
}
#[test]
fn comparison_is_exact_at_the_edges_of_the_range() {
let max = at("9007199254740991");
assert!(!max.is_not_yet_valid(MAX_SAFE_INTEGER, 600));
assert!(max.is_not_yet_valid(MAX_SAFE_INTEGER - 1, 0));
assert!(max.is_expired(MAX_SAFE_INTEGER, 0));
assert!(!max.is_expired(MAX_SAFE_INTEGER - 1, 0));
let min = at("-9007199254740991");
assert!(min.is_expired(-MAX_SAFE_INTEGER + 600, 600));
assert!(!min.is_expired(-MAX_SAFE_INTEGER - 1, 0));
assert!(min.is_expired(i64::MAX, u32::MAX));
assert!(!max.is_not_yet_valid(i64::MAX, u32::MAX));
assert!(max.is_not_yet_valid(i64::MIN, u32::MAX));
assert!(!min.is_expired(i64::MIN, u32::MAX));
}
#[test]
fn cmp_secs_orders_fractions_between_their_integers() {
let half = at("1000.5");
assert_eq!(half.cmp_secs(1000), Ordering::Greater);
assert_eq!(half.cmp_secs(1001), Ordering::Less);
assert_eq!(at("1000").cmp_secs(1000), Ordering::Equal);
let neg = at("-0.5");
assert_eq!(neg.cmp_secs(0), Ordering::Less);
assert_eq!(neg.cmp_secs(-1), Ordering::Greater);
}
}