use serde::{Deserializer, Serialize, Serializer, de};
use crate::platform::{Duration, SystemTime};
fn to_unix<E: serde::ser::Error>(t: SystemTime) -> Result<u64, E> {
t.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.map_err(|_| E::custom("SystemTime is before the Unix epoch"))
}
fn from_unix<E: serde::de::Error>(secs: u64) -> Result<SystemTime, E> {
SystemTime::UNIX_EPOCH
.checked_add(Duration::from_secs(secs))
.ok_or_else(|| E::custom("Unix timestamp overflows SystemTime"))
}
pub(crate) struct SecsVisitor;
impl de::Visitor<'_> for SecsVisitor {
type Value = u64;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a non-negative numeric Unix timestamp in seconds")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<u64, E> {
Ok(v)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<u64, E> {
if v < 0 {
return Err(E::custom("Unix timestamp cannot be negative"));
}
Ok(v.cast_unsigned())
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_sign_loss)]
fn visit_f64<E: de::Error>(self, v: f64) -> Result<u64, E> {
if v.is_nan() {
return Err(E::custom("Unix timestamp cannot be NaN"));
}
if v < 0.0 || v >= u64::MAX as f64 {
return Err(E::custom("Unix timestamp outside u64 range"));
}
Ok(v as u64)
}
}
pub mod unix_secs {
use super::{Deserializer, SecsVisitor, Serialize, Serializer, SystemTime, from_unix, to_unix};
pub fn serialize<S: Serializer>(time: &SystemTime, ser: S) -> Result<S::Ok, S::Error> {
to_unix(*time)?.serialize(ser)
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<SystemTime, D::Error> {
de.deserialize_any(SecsVisitor).and_then(from_unix)
}
}
pub mod option_unix_secs {
use super::{
Deserializer, SecsVisitor, Serialize, Serializer, SystemTime, de, from_unix, to_unix,
};
pub fn serialize<S: Serializer>(time: &Option<SystemTime>, ser: S) -> Result<S::Ok, S::Error> {
let secs = match *time {
Some(t) => Some(to_unix(t)?),
None => None,
};
secs.serialize(ser)
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Option<SystemTime>, D::Error> {
struct OptVisitor;
impl<'de> de::Visitor<'de> for OptVisitor {
type Value = Option<SystemTime>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a non-negative numeric Unix timestamp in seconds, or null")
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
d.deserialize_any(SecsVisitor).and_then(from_unix).map(Some)
}
}
de.deserialize_option(OptVisitor)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use serde::{
Deserialize, Serialize,
de::{
IntoDeserializer,
value::{Error as ValueError, F64Deserializer},
},
};
use serde_json::json;
use super::*;
#[derive(Debug, Serialize, Deserialize)]
struct WithTime {
#[serde(with = "unix_secs")]
t: SystemTime,
}
#[derive(Debug, Serialize, Deserialize)]
struct WithOptTime {
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "option_unix_secs"
)]
t: Option<SystemTime>,
}
fn epoch_plus(secs: u64) -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(secs)
}
#[test]
fn unix_secs_roundtrip_integer() {
let t = epoch_plus(1_700_000_000);
let v = serde_json::to_value(WithTime { t }).unwrap();
assert_eq!(v, json!({ "t": 1_700_000_000_u64 }));
let parsed: WithTime = serde_json::from_value(v).unwrap();
assert_eq!(parsed.t, t);
}
#[test]
fn unix_secs_serialize_drops_sub_second() {
let t = SystemTime::UNIX_EPOCH + Duration::new(42, 999_999_999);
let v = serde_json::to_value(WithTime { t }).unwrap();
assert_eq!(v, json!({ "t": 42_u64 }));
}
#[test]
fn unix_secs_serialize_pre_epoch_errors() {
let Some(t) = SystemTime::UNIX_EPOCH.checked_sub(Duration::from_secs(1)) else {
return;
};
let err = serde_json::to_value(WithTime { t }).unwrap_err();
assert!(err.to_string().contains("before the Unix epoch"));
}
#[test]
fn unix_secs_deserialize_float_truncates() {
let parsed: WithTime = serde_json::from_value(json!({ "t": 1234.9 })).unwrap();
assert_eq!(parsed.t, epoch_plus(1234));
}
#[rstest]
#[case::negative(json!({ "t": -1 }), "negative")]
#[case::overflows_systemtime(json!({ "t": u64::MAX }), "overflows")]
fn unix_secs_deserialize_errors(#[case] input: serde_json::Value, #[case] needle: &str) {
let err = serde_json::from_value::<WithTime>(input).unwrap_err();
assert!(
err.to_string().contains(needle),
"expected error containing {needle:?}, got: {err}"
);
}
#[rstest]
#[case::outside_u64_range(2.0_f64.powi(64), "outside u64 range")]
#[case::nan(f64::NAN, "NaN")]
fn secs_visitor_rejects_invalid_float(#[case] input: f64, #[case] needle: &str) {
let de: F64Deserializer<ValueError> = input.into_deserializer();
let err = de.deserialize_any(SecsVisitor).unwrap_err();
assert!(
err.to_string().contains(needle),
"expected error containing {needle:?}, got: {err}"
);
}
#[test]
fn option_unix_secs_roundtrip_some() {
let t = epoch_plus(1_700_000_000);
let v = serde_json::to_value(WithOptTime { t: Some(t) }).unwrap();
assert_eq!(v, json!({ "t": 1_700_000_000_u64 }));
let parsed: WithOptTime = serde_json::from_value(v).unwrap();
assert_eq!(parsed.t, Some(t));
}
#[test]
fn option_unix_secs_serialize_none_omitted_with_skip() {
let v = serde_json::to_value(WithOptTime { t: None }).unwrap();
assert_eq!(v, json!({}));
}
#[test]
fn option_unix_secs_deserialize_null() {
let parsed: WithOptTime = serde_json::from_value(json!({ "t": null })).unwrap();
assert_eq!(parsed.t, None);
}
#[test]
fn option_unix_secs_deserialize_absent() {
let parsed: WithOptTime = serde_json::from_value(json!({})).unwrap();
assert_eq!(parsed.t, None);
}
#[test]
fn option_unix_secs_deserialize_negative_errors() {
let err = serde_json::from_value::<WithOptTime>(json!({ "t": -1 })).unwrap_err();
assert!(err.to_string().contains("negative"));
}
}