use std::{error::Error, fmt, time::Duration};
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike, Utc};
use jiff::{
Timestamp, civil,
tz::{AmbiguousOffset, TimeZone},
};
use num_bigint::BigInt;
use crate::object::MontyTimeZone;
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct OsPolicy {
pub datetime: DateTimeSource,
pub timezone: SandboxTimeZone,
pub sleep: SleepMode,
pub process_time: ProcessTime,
pub random_start: RandomStart,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum DateTimeSource {
#[default]
System,
CallHost,
Fixed {
unix_seconds: i64,
microsecond: u32,
},
}
impl DateTimeSource {
#[must_use]
pub fn read(self) -> Option<NaiveDateTime> {
let utc = match self {
Self::System => Utc::now().naive_utc(),
Self::CallHost => return None,
Self::Fixed {
unix_seconds,
microsecond,
} => {
let nanoseconds = microsecond.checked_mul(1_000).filter(|ns| *ns < 1_000_000_000)?;
DateTime::from_timestamp(unix_seconds, nanoseconds)?.naive_utc()
}
};
local_wall_clock(utc, 0).map(|_| utc)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SandboxTimeZone {
Fixed {
offset_seconds: i32,
name: Option<String>,
},
Named(NamedZone),
}
impl Default for SandboxTimeZone {
fn default() -> Self {
Self::utc()
}
}
impl SandboxTimeZone {
#[must_use]
pub fn utc() -> Self {
Self::Fixed {
offset_seconds: 0,
name: Some("UTC".to_owned()),
}
}
pub fn named(name: &str) -> Result<Self, UnknownTimeZone> {
NamedZone::resolve(name).map(Self::Named)
}
#[must_use]
pub fn iana_name(&self) -> Option<&str> {
match self {
Self::Fixed { .. } => None,
Self::Named(zone) => Some(zone.name()),
}
}
#[must_use]
pub fn at(&self, utc: NaiveDateTime) -> MontyTimeZone {
match self {
Self::Fixed { offset_seconds, name } => MontyTimeZone {
offset_seconds: *offset_seconds,
name: name.clone(),
},
Self::Named(zone) => {
let info = zone.zone.to_offset_info(timestamp(utc));
MontyTimeZone {
offset_seconds: info.offset().seconds(),
name: Some(info.abbreviation().to_owned()),
}
}
}
}
#[must_use]
pub fn offset_for_local(&self, local: NaiveDateTime) -> Option<i32> {
match self {
Self::Fixed { offset_seconds, .. } => Some(*offset_seconds),
Self::Named(zone) => match zone.zone.to_ambiguous_timestamp(civil_datetime(local)?).offset() {
AmbiguousOffset::Unambiguous { offset }
| AmbiguousOffset::Gap { before: offset, .. }
| AmbiguousOffset::Fold { before: offset, .. } => Some(offset.seconds()),
},
}
}
#[must_use]
pub fn utc_from_local(&self, local: NaiveDateTime) -> Option<NaiveDateTime> {
local_wall_clock(local, self.offset_for_local(local)?.checked_neg()?)
}
#[must_use]
pub fn constants(&self, year: Option<i32>) -> Option<ZoneConstants> {
let (january, july) = match self {
Self::Fixed { .. } => {
let zone = self.at(DateTime::UNIX_EPOCH.naive_utc());
(zone.clone(), zone)
}
Self::Named(_) => {
let year = year?;
let at = |month| Some(self.at(NaiveDate::from_ymd_opt(year, month, 1)?.and_time(NaiveTime::MIN)));
(at(1)?, at(7)?)
}
};
let (standard, daylight) = if january.offset_seconds < july.offset_seconds {
(january, july)
} else {
(july, january)
};
Some(ZoneConstants {
daylight: standard.offset_seconds != daylight.offset_seconds,
standard,
daylight_zone: daylight,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct NamedZone {
name: String,
zone: TimeZone,
}
impl NamedZone {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
fn resolve(name: &str) -> Result<Self, UnknownTimeZone> {
let valid_part = |part: &str| {
!matches!(part, "" | "." | "..") && part.bytes().all(|b| b.is_ascii_alphanumeric() || b"._+-".contains(&b))
};
let unknown = || UnknownTimeZone(name.to_owned());
if name.len() > 100 || !name.split('/').all(valid_part) {
return Err(unknown());
}
let zone = TimeZone::get(name).map_err(|_| unknown())?;
let name = zone.iana_name().ok_or_else(unknown)?.to_owned();
Ok(Self { name, zone })
}
}
impl TryFrom<String> for NamedZone {
type Error = UnknownTimeZone;
fn try_from(name: String) -> Result<Self, Self::Error> {
Self::resolve(&name)
}
}
impl From<NamedZone> for String {
fn from(zone: NamedZone) -> Self {
zone.name
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownTimeZone(pub String);
impl fmt::Display for UnknownTimeZone {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown timezone '{}'", self.0)
}
}
impl Error for UnknownTimeZone {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZoneConstants {
pub standard: MontyTimeZone,
pub daylight_zone: MontyTimeZone,
pub daylight: bool,
}
fn timestamp(utc: NaiveDateTime) -> Timestamp {
let utc = utc.and_utc();
let seconds = utc.timestamp();
let nanoseconds = i32::try_from(utc.timestamp_subsec_nanos()).unwrap_or(0);
Timestamp::new(seconds, nanoseconds).unwrap_or(if seconds < 0 { Timestamp::MIN } else { Timestamp::MAX })
}
fn civil_datetime(local: NaiveDateTime) -> Option<civil::DateTime> {
civil::DateTime::new(
i16::try_from(local.year()).ok()?,
i8::try_from(local.month()).ok()?,
i8::try_from(local.day()).ok()?,
i8::try_from(local.hour()).ok()?,
i8::try_from(local.minute()).ok()?,
i8::try_from(local.second()).ok()?,
i32::try_from(local.nanosecond()).ok()?,
)
.ok()
}
#[must_use]
pub fn local_wall_clock(utc: NaiveDateTime, offset_seconds: i32) -> Option<NaiveDateTime> {
let shifted = utc.checked_add_signed(TimeDelta::seconds(i64::from(offset_seconds)))?;
(1..=9999).contains(&shifted.year()).then_some(shifted)
}
#[must_use]
pub fn unix_seconds(utc: NaiveDateTime) -> f64 {
let epoch = utc.and_utc();
epoch.timestamp() as f64 + f64::from(epoch.timestamp_subsec_micros()) / 1_000_000.0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SleepMode {
System(Duration),
CallHost,
Zero,
}
impl SleepMode {
pub const DEFAULT_MAX: Duration = Duration::from_secs(10);
}
impl Default for SleepMode {
fn default() -> Self {
Self::System(Self::DEFAULT_MAX)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum ProcessTime {
#[default]
Zero,
Elapsed,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum RandomStart {
#[default]
System,
CallHost,
Seed(RandomSeed),
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum RandomSeed {
Int(BigInt),
Float(f64),
Str(String),
Bytes(#[serde(with = "serde_bytes")] Vec<u8>),
}