#![allow(clippy::doc_markdown, clippy::missing_errors_doc)]
#[cfg(test)]
mod test;
use std::{process::Command, str};
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use thiserror::Error;
use time::{
error::{ComponentRange, Format},
format_description::FormatItem,
macros::format_description,
Duration, OffsetDateTime, UtcOffset,
};
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct Errors(Vec<Error>);
impl Errors {
fn new() -> Self {
Self(vec![])
}
fn push(&mut self, e: Error) {
self.0.push(e)
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("Unable to acquire a write lock")]
WriteLock,
#[error("Unable to acquire a read lock")]
ReadLock,
#[error("Unable to parse time: {0}")]
Parse(#[from] time::error::Parse),
#[error("Unable to construct offset from offset hours/minutes: {0}")]
Time(#[from] ComponentRange),
#[error("Unable to format timestamp: {0}")]
TimeFormat(#[from] Format),
#[error("Invalid offset hours: {0}")]
InvalidOffsetHours(i8),
#[error("Invalid offset minutes: {0}")]
InvalidOffsetMinutes(i8),
#[error("Unable to parse offset string")]
InvalidOffsetString,
#[error("Error executing command to get system time: {0}")]
TimeCommand(std::io::Error),
#[error("Datetime overflow")]
DatetimeOverflow,
#[error("The global offset is not initialized.")]
Uninitialized,
}
static OFFSET: OnceCell<RwLock<UtcOffset>> = OnceCell::new();
const TIME_FORMAT: &[FormatItem<'static>] = format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_second]"
);
const PARSE_FORMAT: &[FormatItem<'static>] =
format_description!("[offset_hour][optional [:]][offset_minute]");
#[inline]
pub fn get_global_offset() -> Result<UtcOffset> {
if let Some(o) = OFFSET.get() {
Ok(*o.read())
} else {
Err(Error::Uninitialized)
}
}
#[inline]
pub fn try_set_global_offset(o: UtcOffset) -> Result<()> {
let o_ref = OFFSET.get_or_init(|| RwLock::new(o));
if let Some(mut o_lock) = o_ref.try_write() {
*o_lock = o;
Ok(())
} else {
Err(Error::WriteLock)
}
}
#[inline]
pub fn try_set_global_offset_from_str(input: &str) -> Result<()> {
let trimmed = trim_new_lines(input);
let o = UtcOffset::parse(trimmed, &PARSE_FORMAT).map_err(|_| Error::InvalidOffsetString)?;
try_set_global_offset(o)
}
#[allow(clippy::manual_range_contains)]
#[inline]
pub fn try_set_global_offset_from_pair(offset_hours: i8, offset_minutes: i8) -> Result<()> {
let o = from_offset_pair(offset_hours, offset_minutes)?;
try_set_global_offset(o)
}
#[inline]
pub fn get_local_timestamp_rfc3339() -> Result<(String, Errors)> {
let (offset, errs) = get_utc_offset();
let res = get_local_timestamp_from_offset_rfc3339(offset)?;
Ok((res, errs))
}
#[allow(clippy::cast_lossless)]
#[inline]
pub fn get_local_timestamp_from_offset_rfc3339(utc_offset: UtcOffset) -> Result<String> {
let dt_now = OffsetDateTime::now_utc();
let offset_dt_now = if utc_offset == UtcOffset::UTC {
dt_now
} else if let Some(t) = dt_now.checked_add(Duration::minutes(utc_offset.whole_minutes() as i64))
{
t.replace_offset(utc_offset)
} else {
return Err(Error::DatetimeOverflow);
};
let formatted = offset_dt_now.format(&TIME_FORMAT)?;
Ok(formatted)
}
#[inline]
pub fn get_utc_offset() -> (UtcOffset, Errors) {
let mut errs = Errors::new();
if let Ok(o) = get_global_offset() {
return (o, errs);
}
let o = match construct_offset() {
Ok(o) => o,
Err(e) => {
errs.push(e);
UtcOffset::UTC
}
};
if let Err(e) = try_set_global_offset(o) {
errs.push(e)
}
(o, errs)
}
fn parse_cmd_output(stdout: &[u8], formatter: &[FormatItem<'static>]) -> Result<UtcOffset> {
let output = String::from_utf8_lossy(stdout);
let trimmed = trim_new_lines(&output);
let offset = UtcOffset::parse(trimmed, &formatter)?;
Ok(offset)
}
fn offset_from_process() -> Result<UtcOffset> {
let cmd = if cfg!(target_os = "windows") {
|| {
Command::new("powershell")
.arg("Get-Date")
.arg("-Format")
.arg("\"K \"")
.output()
}
} else {
|| Command::new("date").arg("+%z").output()
};
match cmd() {
Ok(output) => parse_cmd_output(&output.stdout, PARSE_FORMAT),
Err(e) => Err(Error::TimeCommand(e)),
}
}
fn trim_new_lines(s: &str) -> &str {
s.trim().trim_end_matches("\r\n").trim_matches('\n')
}
fn from_offset_pair(offset_hours: i8, offset_minutes: i8) -> Result<UtcOffset> {
if !(-12..=14).contains(&offset_hours) {
return Err(Error::InvalidOffsetHours(offset_hours));
} else if !(0..=59).contains(&offset_minutes) {
return Err(Error::InvalidOffsetMinutes(offset_minutes));
}
Ok(UtcOffset::from_hms(offset_hours, offset_minutes, 0)?)
}
fn construct_offset() -> Result<UtcOffset> {
UtcOffset::current_local_offset().or_else(|_| offset_from_process())
}