#![allow(clippy::doc_markdown, clippy::missing_errors_doc)]
#[cfg(test)]
mod test;
use std::{process::Command, str, sync::RwLock};
use once_cell::sync::Lazy;
use thiserror::Error;
use time::{
error::{ComponentRange, Format},
format_description::{parse, FormatItem},
Duration, OffsetDateTime, UtcOffset,
};
#[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 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,
}
static OFFSET: Lazy<RwLock<Option<UtcOffset>>> = Lazy::new(|| RwLock::new(None));
static TIME_FORMAT: Lazy<Vec<FormatItem<'static>>> = Lazy::new(|| {
parse(
"[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_second]",
)
.unwrap_or_default()
});
static PARSE_FORMAT: Lazy<Vec<FormatItem<'static>>> =
Lazy::new(|| parse("[offset_hour][offset_minute]").unwrap_or_default());
static PARSE_FORMAT_WITH_COLON: Lazy<Vec<FormatItem<'static>>> =
Lazy::new(|| parse("[offset_hour]:[offset_minute]").unwrap_or_default());
pub fn set_global_offset_from_str(input: &str) -> Result<(i8, i8), Error> {
let trimmed = trim_new_lines(input);
if let Ok(o) = UtcOffset::parse(trimmed, &PARSE_FORMAT) {
init_from_utc_offset(o)
} else if let Ok(o) = UtcOffset::parse(trimmed, &PARSE_FORMAT_WITH_COLON) {
init_from_utc_offset(o)
} else {
Err(Error::InvalidOffsetString)
}
}
#[allow(clippy::manual_range_contains)]
pub fn set_global_offset(offset_hours: i8, offset_minutes: i8) -> Result<(i8, i8), Error> {
if offset_hours < -12 || offset_hours > 14 {
Err(Error::InvalidOffsetHours(offset_hours))
} else if !(0..=59).contains(&offset_minutes) {
Err(Error::InvalidOffsetMinutes(offset_minutes))
} else {
let o = UtcOffset::from_hms(offset_hours, offset_minutes, 0)?;
init_from_utc_offset(o)
}
}
pub fn get_local_timestamp_rfc3339() -> Result<String, Error> {
get_local_timestamp_from_offset_rfc3339(get_local_offset())
}
#[allow(clippy::cast_lossless)]
pub fn get_local_timestamp_from_offset_rfc3339(utc_offset: UtcOffset) -> Result<String, Error> {
let datetime_now = OffsetDateTime::now_utc();
if utc_offset != UtcOffset::UTC {
if let Some(t) = datetime_now.checked_add(Duration::hours(utc_offset.whole_hours() as i64))
{
let offset_datetime_now = t.replace_offset(utc_offset);
return Ok(offset_datetime_now.format(&TIME_FORMAT)?);
}
}
Ok(datetime_now.format(&TIME_FORMAT)?)
}
fn init_from_utc_offset(offset: UtcOffset) -> Result<(i8, i8), Error> {
if let Ok(mut l) = OFFSET.write() {
*l = Some(offset);
} else {
log::warn!("UTC Offset failed: {}", offset);
return Err(Error::WriteLock);
}
log::info!("UTC Offset set to: {}", offset);
Ok((offset.whole_hours(), offset.minutes_past_hour()))
}
pub fn get_local_offset() -> UtcOffset {
if let Ok(reader) = OFFSET.read() {
if let Some(o) = *reader {
return o;
}
}
let offset = if let Ok(o) = time::UtcOffset::current_local_offset() {
o
} else if let Some(o) = offset_from_process() {
o
} else {
UtcOffset::UTC
};
if let Err(e) = init_from_utc_offset(offset) {
log::warn!("Unable to initialize offset: {}", e);
}
offset
}
fn process_cmd_output(stdout: &[u8], formatter: &[FormatItem<'static>]) -> Option<UtcOffset> {
match str::from_utf8(stdout) {
Ok(v) => match UtcOffset::parse(trim_new_lines(v), &formatter) {
Ok(o) => return Some(o),
Err(e) => {
log::warn!("Unable to parse output: {}", e);
}
},
Err(e) => {
log::warn!("Unable to convert output: {}", e);
}
}
None
}
fn offset_from_process() -> Option<UtcOffset> {
if cfg!(target_os = "windows") {
if let Ok(output) = Command::new("powershell")
.arg("Get-Date")
.arg("-Format")
.arg("\"K \"")
.output()
{
return process_cmd_output(&output.stdout, &PARSE_FORMAT_WITH_COLON);
}
} else if let Ok(output) = Command::new("date").arg("+%z").output() {
return process_cmd_output(&output.stdout, &PARSE_FORMAT);
}
None
}
fn trim_new_lines(s: &str) -> &str {
s.trim().trim_end_matches("\r\n").trim_matches('\n')
}