1#![allow(clippy::doc_markdown, clippy::missing_errors_doc)]
2
3#[cfg(test)]
4mod test;
5
6use std::{process::Command, str};
7
8use once_cell::sync::OnceCell;
9use parking_lot::RwLock;
10use thiserror::Error;
11use time::{
12 error::{ComponentRange, Format},
13 format_description::FormatItem,
14 macros::format_description,
15 Duration, OffsetDateTime, UtcOffset,
16};
17
18pub type Result<T, E = Error> = std::result::Result<T, E>;
20
21#[derive(Debug)]
23pub struct Errors(Vec<Error>);
24impl Errors {
25 fn new() -> Self {
26 Self(vec![])
27 }
28 fn push(&mut self, e: Error) {
29 self.0.push(e)
30 }
31}
32
33#[derive(Error, Debug)]
35pub enum Error {
36 #[error("Unable to acquire a write lock")]
38 WriteLock,
39
40 #[error("Unable to acquire a read lock")]
42 ReadLock,
43
44 #[error("Unable to parse time: {0}")]
46 Parse(#[from] time::error::Parse),
47
48 #[error("Unable to construct offset from offset hours/minutes: {0}")]
50 Time(#[from] ComponentRange),
51
52 #[error("Unable to format timestamp: {0}")]
55 TimeFormat(#[from] Format),
56
57 #[error("Invalid offset hours: {0}")]
59 InvalidOffsetHours(i8),
60
61 #[error("Invalid offset minutes: {0}")]
63 InvalidOffsetMinutes(i8),
64
65 #[error("Unable to parse offset string")]
67 InvalidOffsetString,
68
69 #[error("Error executing command to get system time: {0}")]
71 TimeCommand(std::io::Error),
72
73 #[error("Datetime overflow")]
75 DatetimeOverflow,
76
77 #[error("The global offset is not initialized.")]
79 Uninitialized,
80}
81
82static OFFSET: OnceCell<RwLock<UtcOffset>> = OnceCell::new();
83const TIME_FORMAT: &[FormatItem<'static>] = format_description!(
84 "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_second]"
85);
86const PARSE_FORMAT: &[FormatItem<'static>] =
87 format_description!("[offset_hour][optional [:]][offset_minute]");
88
89#[inline]
92pub fn get_global_offset() -> Result<UtcOffset> {
93 if let Some(o) = OFFSET.get() {
94 Ok(*o.read())
95 } else {
96 Err(Error::Uninitialized)
97 }
98}
99#[inline]
102pub fn try_set_global_offset(o: UtcOffset) -> Result<()> {
103 let o_ref = OFFSET.get_or_init(|| RwLock::new(o));
104 if let Some(mut o_lock) = o_ref.try_write() {
105 *o_lock = o;
106 Ok(())
107 } else {
108 Err(Error::WriteLock)
109 }
110}
111
112#[inline]
122pub fn try_set_global_offset_from_str(input: &str) -> Result<()> {
123 let trimmed = trim_new_lines(input);
124 let o = UtcOffset::parse(trimmed, &PARSE_FORMAT).map_err(|_| Error::InvalidOffsetString)?;
125 try_set_global_offset(o)
126}
127
128#[allow(clippy::manual_range_contains)]
140#[inline]
141pub fn try_set_global_offset_from_pair(offset_hours: i8, offset_minutes: i8) -> Result<()> {
142 let o = from_offset_pair(offset_hours, offset_minutes)?;
143 try_set_global_offset(o)
144}
145
146#[inline]
160pub fn get_local_timestamp_rfc3339() -> Result<(String, Errors)> {
161 let (offset, errs) = get_utc_offset();
162 let res = get_local_timestamp_from_offset_rfc3339(offset)?;
163 Ok((res, errs))
164}
165
166#[allow(clippy::cast_lossless)]
177#[inline]
178pub fn get_local_timestamp_from_offset_rfc3339(utc_offset: UtcOffset) -> Result<String> {
179 let dt_now = OffsetDateTime::now_utc();
180 let offset_dt_now = if utc_offset == UtcOffset::UTC {
181 dt_now
182 } else if let Some(t) = dt_now.checked_add(Duration::minutes(utc_offset.whole_minutes() as i64))
183 {
184 t.replace_offset(utc_offset)
185 } else {
186 return Err(Error::DatetimeOverflow);
188 };
189
190 let formatted = offset_dt_now.format(&TIME_FORMAT)?;
191 Ok(formatted)
192}
193
194#[inline]
197pub fn get_utc_offset() -> (UtcOffset, Errors) {
198 let mut errs = Errors::new();
199 if let Ok(o) = get_global_offset() {
200 return (o, errs);
201 }
202
203 let o = match construct_offset() {
204 Ok(o) => o,
205 Err(e) => {
206 errs.push(e);
207 UtcOffset::UTC
208 }
209 };
210
211 if let Err(e) = try_set_global_offset(o) {
212 errs.push(e)
213 }
214 (o, errs)
215}
216
217fn parse_cmd_output(stdout: &[u8], formatter: &[FormatItem<'static>]) -> Result<UtcOffset> {
218 let output = String::from_utf8_lossy(stdout);
219 let trimmed = trim_new_lines(&output);
220 let offset = UtcOffset::parse(trimmed, &formatter)?;
221 Ok(offset)
222}
223
224fn offset_from_process() -> Result<UtcOffset> {
225 let cmd = if cfg!(target_os = "windows") {
226 || {
227 Command::new("powershell")
228 .arg("Get-Date")
229 .arg("-Format")
230 .arg("\"K \"")
231 .output()
232 }
233 } else {
234 || Command::new("date").arg("+%z").output()
235 };
236
237 match cmd() {
238 Ok(output) => parse_cmd_output(&output.stdout, PARSE_FORMAT),
239 Err(e) => Err(Error::TimeCommand(e)),
240 }
241}
242
243fn trim_new_lines(s: &str) -> &str {
244 s.trim().trim_end_matches("\r\n").trim_matches('\n')
245}
246
247fn from_offset_pair(offset_hours: i8, offset_minutes: i8) -> Result<UtcOffset> {
248 if !(-12..=14).contains(&offset_hours) {
249 return Err(Error::InvalidOffsetHours(offset_hours));
250 } else if !(0..=59).contains(&offset_minutes) {
251 return Err(Error::InvalidOffsetMinutes(offset_minutes));
252 }
253
254 Ok(UtcOffset::from_hms(offset_hours, offset_minutes, 0)?)
255}
256
257fn construct_offset() -> Result<UtcOffset> {
259 UtcOffset::current_local_offset().or_else(|_| offset_from_process())
260}