1 #![allow(clippy::manual_range_contains)]
3 use std
::ffi
::{CStr, CString}
;
5 use anyhow
::{bail, format_err, Error}
;
7 /// Safe bindings to libc timelocal
9 /// We set tm_isdst to -1.
10 /// This also normalizes the parameter
11 pub fn timelocal(t
: &mut libc
::tm
) -> Result
<i64, Error
> {
14 let epoch
= unsafe { libc::mktime(t) }
;
16 bail
!("libc::mktime failed for {:?}", t
);
21 /// Safe bindings to libc timegm
23 /// We set tm_isdst to 0.
24 /// This also normalizes the parameter
25 pub fn timegm(t
: &mut libc
::tm
) -> Result
<i64, Error
> {
28 let epoch
= unsafe { libc::timegm(t) }
;
30 bail
!("libc::timegm failed for {:?}", t
);
35 fn new_libc_tm() -> libc
::tm
{
47 tm_zone
: std
::ptr
::null(),
51 /// Safe bindings to libc localtime
52 pub fn localtime(epoch
: i64) -> Result
<libc
::tm
, Error
> {
53 let mut result
= new_libc_tm();
56 if libc
::localtime_r(&epoch
, &mut result
).is_null() {
57 bail
!("libc::localtime failed for '{}'", epoch
);
64 /// Safe bindings to libc gmtime
65 pub fn gmtime(epoch
: i64) -> Result
<libc
::tm
, Error
> {
66 let mut result
= new_libc_tm();
69 if libc
::gmtime_r(&epoch
, &mut result
).is_null() {
70 bail
!("libc::gmtime failed for '{}'", epoch
);
77 /// Returns Unix Epoch (now)
79 /// Note: This panics if the SystemTime::now() returns values not
80 /// repesentable as i64 (should never happen).
81 pub fn epoch_i64() -> i64 {
82 use std
::time
::{SystemTime, UNIX_EPOCH}
;
84 let now
= SystemTime
::now();
87 i64::try_from(now
.duration_since(UNIX_EPOCH
).unwrap().as_secs())
88 .expect("epoch_i64: now is too large")
90 -i64::try_from(UNIX_EPOCH
.duration_since(now
).unwrap().as_secs())
91 .expect("epoch_i64: now is too small")
95 /// Returns Unix Epoch (now) as f64 with subseconds resolution
97 /// Note: This can be inacurrate for values greater the 2^53. But this
98 /// should never happen.
99 pub fn epoch_f64() -> f64 {
100 use std
::time
::{SystemTime, UNIX_EPOCH}
;
102 let now
= SystemTime
::now();
104 if now
> UNIX_EPOCH
{
105 now
.duration_since(UNIX_EPOCH
).unwrap().as_secs_f64()
107 -UNIX_EPOCH
.duration_since(now
).unwrap().as_secs_f64()
111 // rust libc bindings do not include strftime
114 #[link_name = "strftime"]
116 s
: *mut libc
::c_char
,
118 format
: *const libc
::c_char
,
119 time
: *const libc
::tm
,
123 /// Safe bindings to libc strftime
124 pub fn strftime(format
: &str, t
: &libc
::tm
) -> Result
<String
, Error
> {
125 let format
= CString
::new(format
).map_err(|err
| format_err
!("{}", err
))?
;
126 let mut buf
= vec
![0u8; 8192];
130 buf
.as_mut_ptr() as *mut libc
::c_char
,
131 buf
.len() as libc
::size_t
,
133 t
as *const libc
::tm
,
137 // -1,, it's unsigned
138 bail
!("strftime failed");
141 // `res` is a `libc::size_t`, which on a different target architecture might not be directly
142 // assignable to a `usize`. Thus, we actually want a cast here.
143 #[allow(clippy::unnecessary_cast)]
144 let len
= res
as usize;
147 bail
!("strftime: result len is 0 (string too large)");
150 let c_str
= CStr
::from_bytes_with_nul(&buf
[..len
+ 1]).map_err(|err
| format_err
!("{}", err
))?
;
151 let str_slice
: &str = c_str
.to_str().unwrap();
152 Ok(str_slice
.to_owned())
155 /// Format epoch as local time
156 pub fn strftime_local(format
: &str, epoch
: i64) -> Result
<String
, Error
> {
157 let localtime
= localtime(epoch
)?
;
158 strftime(format
, &localtime
)
161 /// Format epoch as utc time
162 pub fn strftime_utc(format
: &str, epoch
: i64) -> Result
<String
, Error
> {
163 let gmtime
= gmtime(epoch
)?
;
164 strftime(format
, &gmtime
)
167 /// Convert Unix epoch into RFC3339 UTC string
168 pub fn epoch_to_rfc3339_utc(epoch
: i64) -> Result
<String
, Error
> {
169 let gmtime
= gmtime(epoch
)?
;
171 let year
= gmtime
.tm_year
+ 1900;
172 if year
< 0 || year
> 9999 {
173 bail
!("epoch_to_rfc3339_utc: wrong year '{}'", year
);
176 strftime("%010FT%TZ", &gmtime
)
179 /// Convert Unix epoch into RFC3339 local time with TZ
180 pub fn epoch_to_rfc3339(epoch
: i64) -> Result
<String
, Error
> {
181 use std
::fmt
::Write
as _
;
183 let localtime
= localtime(epoch
)?
;
185 let year
= localtime
.tm_year
+ 1900;
186 if year
< 0 || year
> 9999 {
187 bail
!("epoch_to_rfc3339: wrong year '{}'", year
);
190 // Note: We cannot use strftime %z because of missing collon
192 let mut offset
= localtime
.tm_gmtoff
;
194 let prefix
= if offset
< 0 {
201 let mins
= offset
/ 60;
202 let hours
= mins
/ 60;
203 let mins
= mins
% 60;
205 let mut s
= strftime("%10FT%T", &localtime
)?
;
207 let _
= write
!(s
, "{:02}:{:02}", hours
, mins
);
212 /// Parse RFC3339 into Unix epoch
213 pub fn parse_rfc3339(input_str
: &str) -> Result
<i64, Error
> {
214 parse_rfc3339_do(input_str
).map_err(|err
| {
216 "failed to parse rfc3339 timestamp ({:?}) - {}",
223 fn parse_rfc3339_do(input_str
: &str) -> Result
<i64, Error
> {
224 let input
= input_str
.as_bytes();
226 let expect
= |pos
: usize, c
: u8| {
228 bail
!("unexpected char at pos {}", pos
);
233 let digit
= |pos
: usize| -> Result
<i32, Error
> {
234 let digit
= input
[pos
] as i32;
235 if digit
< 48 || digit
> 57 {
236 bail
!("unexpected char at pos {}", pos
);
241 fn check_max(i
: i32, max
: i32) -> Result
<i32, Error
> {
243 bail
!("value too large ({} > {})", i
, max
);
248 if input
.len() < 20 || input
.len() > 25 {
249 bail
!("timestamp of unexpected length");
256 if input
.len() != 20 {
257 bail
!("unexpected length in UTC timestamp");
261 if input
.len() != 25 {
262 bail
!("unexpected length in timestamp");
265 _
=> bail
!("unexpected timezone indicator"),
268 let mut tm
= crate::TmEditor
::new(true);
270 tm
.set_year(digit(0)?
* 1000 + digit(1)?
* 100 + digit(2)?
* 10 + digit(3)?
)?
;
272 tm
.set_mon(check_max(digit(5)?
* 10 + digit(6)?
, 12)?
)?
;
274 tm
.set_mday(check_max(digit(8)?
* 10 + digit(9)?
, 31)?
)?
;
278 tm
.set_hour(check_max(digit(11)?
* 10 + digit(12)?
, 23)?
)?
;
280 tm
.set_min(check_max(digit(14)?
* 10 + digit(15)?
, 59)?
)?
;
282 tm
.set_sec(check_max(digit(17)?
* 10 + digit(18)?
, 60)?
)?
;
284 let epoch
= tm
.into_epoch()?
;
289 let hours
= check_max(digit(20)?
* 10 + digit(21)?
, 23)?
;
291 let mins
= check_max(digit(23)?
* 10 + digit(24)?
, 59)?
;
293 let offset
= (hours
* 3600 + mins
* 60) as i64;
295 let epoch
= match tz
{
296 b'
+'
=> epoch
- offset
,
297 b'
-'
=> epoch
+ offset
,
298 _
=> unreachable
!(), // already checked above
305 fn test_leap_seconds() {
306 let convert_reconvert
= |epoch
| {
308 epoch_to_rfc3339_utc(epoch
).expect("leap second epoch to rfc3339 should work");
311 parse_rfc3339(&rfc3339
).expect("parsing converted leap second epoch should work");
313 assert_eq
!(epoch
, parsed
);
316 // 2005-12-31T23:59:59Z was followed by a leap second
317 let epoch
= 1136073599;
318 convert_reconvert(epoch
);
319 convert_reconvert(epoch
+ 1);
320 convert_reconvert(epoch
+ 2);
322 let parsed
= parse_rfc3339("2005-12-31T23:59:60Z").expect("parsing leap second should work");
323 assert_eq
!(parsed
, epoch
+ 1);
327 fn test_rfc3339_range() {
328 // also tests single-digit years/first decade values
329 let lower
= -62167219200;
330 let lower_str
= "0000-01-01T00:00:00Z";
332 let upper
= 253402300799;
333 let upper_str
= "9999-12-31T23:59:59Z";
336 epoch_to_rfc3339_utc(lower
).expect("converting lower bound of RFC3339 range should work");
337 assert_eq
!(converted
, lower_str
);
340 epoch_to_rfc3339_utc(upper
).expect("converting upper bound of RFC3339 range should work");
341 assert_eq
!(converted
, upper_str
);
344 parse_rfc3339(lower_str
).expect("parsing lower bound of RFC3339 range should work");
345 assert_eq
!(parsed
, lower
);
348 parse_rfc3339(upper_str
).expect("parsing upper bound of RFC3339 range should work");
349 assert_eq
!(parsed
, upper
);
351 epoch_to_rfc3339_utc(lower
- 1)
352 .expect_err("converting below lower bound of RFC3339 range should fail");
354 epoch_to_rfc3339_utc(upper
+ 1)
355 .expect_err("converting above upper bound of RFC3339 range should fail");
357 let first_century
= -59011459201;
358 let first_century_str
= "0099-12-31T23:59:59Z";
360 let converted
= epoch_to_rfc3339_utc(first_century
)
361 .expect("converting epoch representing first century year should work");
362 assert_eq
!(converted
, first_century_str
);
365 parse_rfc3339(first_century_str
).expect("parsing first century string should work");
366 assert_eq
!(parsed
, first_century
);
368 let first_millenium
= -59011459200;
369 let first_millenium_str
= "0100-01-01T00:00:00Z";
371 let converted
= epoch_to_rfc3339_utc(first_millenium
)
372 .expect("converting epoch representing first millenium year should work");
373 assert_eq
!(converted
, first_millenium_str
);
376 parse_rfc3339(first_millenium_str
).expect("parsing first millenium string should work");
377 assert_eq
!(parsed
, first_millenium
);
381 fn test_gmtime_range() {
382 // year must fit into i32
383 let lower
= -67768040609740800;
384 let upper
= 67768036191676799;
386 let mut lower_tm
= gmtime(lower
).expect("gmtime should work as long as years fit into i32");
387 let res
= timegm(&mut lower_tm
).expect("converting back to epoch should work");
388 assert_eq
!(lower
, res
);
390 gmtime(lower
- 1).expect_err("gmtime should fail for years not fitting into i32");
392 let mut upper_tm
= gmtime(upper
).expect("gmtime should work as long as years fit into i32");
393 let res
= timegm(&mut upper_tm
).expect("converting back to epoch should work");
394 assert_eq
!(upper
, res
);
396 gmtime(upper
+ 1).expect_err("gmtime should fail for years not fitting into i32");
400 fn test_timezones() {
401 let input
= "2020-12-30T00:00:00+06:30";
402 let epoch
= 1609263000;
403 let expected_utc
= "2020-12-29T17:30:00Z";
405 let parsed
= parse_rfc3339(input
).expect("parsing failed");
406 assert_eq
!(parsed
, epoch
);
408 let res
= epoch_to_rfc3339_utc(parsed
).expect("converting to RFC failed");
409 assert_eq
!(expected_utc
, res
);