#![cfg_attr(not(any(feature = "std", feature = "parse", feature = "json")), no_std)]
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
#[cfg(test)]
mod tests;
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
extern crate std;
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
use std::{
error, fmt, fs::File, io::Read, str::from_utf8, string::String, string::ToString, vec::Vec,
};
#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
#[cfg(test)]
mod tests_nostd;
#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
extern crate alloc;
#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
use alloc::{str::from_utf8, string::String, string::ToString, vec::Vec};
#[cfg(any(feature = "parse", feature = "json"))]
use chrono::{DateTime, FixedOffset, TimeZone, Utc};
#[cfg(feature = "json")]
use serde::Serialize;
#[cfg(feature = "json")]
mod offset_serializer {
use serde::Serialize;
use std::{format, string::String};
fn offset_to_json(t: chrono::FixedOffset) -> String {
format!("{:?}", t)
}
pub fn serialize<S: serde::Serializer>(
time: &chrono::FixedOffset,
serializer: S,
) -> Result<S::Ok, S::Error> {
offset_to_json(time.clone()).serialize(serializer)
}
}
use byteorder::{ByteOrder, BE};
const MAGIC: u32 = 0x545A6966;
const HEADER_LEN: usize = 0x2C;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TzError {
InvalidTimezone,
InvalidMagic,
BadUtf8String,
UnsupportedFormat,
NoData,
ParseError,
EmptyString,
JsonError,
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
impl fmt::Display for TzError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("TZfile error : ")?;
f.write_str(match self {
TzError::InvalidTimezone => "Invalid timezone",
TzError::InvalidMagic => "Invalid TZfile",
TzError::BadUtf8String => "Bad utf8 string",
TzError::UnsupportedFormat => "Only V2 format is supported",
TzError::NoData => "No data matched the request",
TzError::ParseError => "Parsing error",
TzError::EmptyString => "Empty string",
TzError::JsonError => "Could not convert to json",
})
}
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
impl From<std::io::Error> for TzError {
fn from(_e: std::io::Error) -> TzError {
TzError::InvalidTimezone
}
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
impl From<std::num::ParseIntError> for TzError {
fn from(_e: std::num::ParseIntError) -> TzError {
TzError::ParseError
}
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
impl From<std::str::Utf8Error> for TzError {
fn from(_e: std::str::Utf8Error) -> TzError {
TzError::BadUtf8String
}
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
impl From<TzError> for std::io::Error {
fn from(e: TzError) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, e)
}
}
#[cfg(feature = "json")]
impl From<serde_json::error::Error> for TzError {
fn from(_e: serde_json::error::Error) -> TzError {
TzError::JsonError
}
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
impl error::Error for TzError {}
#[derive(Debug)]
pub struct Tz {
pub tzh_timecnt_data: Vec<i64>,
pub tzh_timecnt_indices: Vec<u8>,
pub tzh_typecnt: Vec<Ttinfo>,
pub tz_abbr: Vec<String>,
#[cfg(any(feature = "parse", feature = "json"))]
name: String,
}
#[derive(Debug)]
pub struct Ttinfo {
pub tt_utoff: isize,
pub tt_isdst: u8,
pub tt_abbrind: u8,
}
#[derive(Debug, PartialEq)]
struct Header {
tzh_ttisutcnt: usize,
tzh_ttisstdcnt: usize,
tzh_leapcnt: usize,
tzh_timecnt: usize,
tzh_typecnt: usize,
tzh_charcnt: usize,
v2_header_start: usize,
}
#[cfg(any(feature = "parse", feature = "json"))]
#[derive(Debug, PartialEq)]
pub struct TransitionTime {
pub time: DateTime<Utc>,
pub utc_offset: isize,
pub isdst: bool,
pub abbreviation: String,
}
#[cfg(feature = "json")]
#[derive(Debug, Serialize)]
pub struct Tzinfo {
pub timezone: String,
pub utc_datetime: DateTime<Utc>,
pub datetime: DateTime<FixedOffset>,
pub dst_from: Option<DateTime<Utc>>,
pub dst_until: Option<DateTime<Utc>>,
pub dst_period: bool,
pub raw_offset: isize,
pub dst_offset: isize,
#[serde(with = "offset_serializer")]
pub utc_offset: FixedOffset,
pub abbreviation: String,
pub week_number: i32,
}
#[cfg(feature = "parse")]
#[derive(Debug)]
pub struct Tzinfo {
pub timezone: String,
pub utc_datetime: DateTime<Utc>,
pub datetime: DateTime<FixedOffset>,
pub dst_from: Option<DateTime<Utc>>,
pub dst_until: Option<DateTime<Utc>>,
pub dst_period: bool,
pub raw_offset: isize,
pub dst_offset: isize,
pub utc_offset: FixedOffset,
pub abbreviation: String,
pub week_number: i32,
}
#[cfg(feature = "json")]
impl Tzinfo {
pub fn to_json(&self) -> Result<String, serde_json::error::Error> {
serde_json::to_string(self)
}
}
impl Tz {
#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
pub fn new(buf: Vec<u8>) -> Result<Tz, TzError> {
let header = Tz::parse_header(&buf)?;
Tz::parse_data(&buf, header)
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
pub fn new(tz: &str) -> Result<Tz, TzError> {
let buf = Tz::read(tz)?;
let header = Tz::parse_header(&buf)?;
Tz::parse_data(&buf, header, tz)
}
fn parse_header(buffer: &[u8]) -> Result<Header, TzError> {
let magic = BE::read_u32(&buffer[0x00..=0x03]);
if magic != MAGIC {
return Err(TzError::InvalidMagic);
}
if buffer[4] != 50 {
return Err(TzError::UnsupportedFormat);
}
let tzh_ttisutcnt = BE::read_i32(&buffer[0x14..=0x17]) as usize;
let tzh_ttisstdcnt = BE::read_i32(&buffer[0x18..=0x1B]) as usize;
let tzh_leapcnt = BE::read_i32(&buffer[0x1C..=0x1F]) as usize;
let tzh_timecnt = BE::read_i32(&buffer[0x20..=0x23]) as usize;
let tzh_typecnt = BE::read_i32(&buffer[0x24..=0x27]) as usize;
let tzh_charcnt = BE::read_i32(&buffer[0x28..=0x2b]) as usize;
let s: usize = tzh_timecnt * 5
+ tzh_typecnt * 6
+ tzh_leapcnt * 8
+ tzh_charcnt
+ tzh_ttisstdcnt
+ tzh_ttisutcnt
+ 44;
Ok(Header {
tzh_ttisutcnt: BE::read_i32(&buffer[s + 0x14..=s + 0x17]) as usize,
tzh_ttisstdcnt: BE::read_i32(&buffer[s + 0x18..=s + 0x1B]) as usize,
tzh_leapcnt: BE::read_i32(&buffer[s + 0x1C..=s + 0x1F]) as usize,
tzh_timecnt: BE::read_i32(&buffer[s + 0x20..=s + 0x23]) as usize,
tzh_typecnt: BE::read_i32(&buffer[s + 0x24..=s + 0x27]) as usize,
tzh_charcnt: BE::read_i32(&buffer[s + 0x28..=s + 0x2b]) as usize,
v2_header_start: s,
})
}
#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
fn parse_data(buffer: &Vec<u8>, header: Header) -> Result<Tz, TzError> {
let tzh_timecnt_len: usize = header.tzh_timecnt * 9;
let tzh_typecnt_len: usize = header.tzh_typecnt * 6;
let tzh_leapcnt_len: usize = header.tzh_leapcnt * 12;
let tzh_charcnt_len: usize = header.tzh_charcnt;
let tzh_timecnt_end: usize = HEADER_LEN + header.v2_header_start + tzh_timecnt_len;
let tzh_typecnt_end: usize = tzh_timecnt_end + tzh_typecnt_len;
let tzh_leapcnt_end: usize = tzh_typecnt_end + tzh_leapcnt_len;
let tzh_charcnt_end: usize = tzh_leapcnt_end + tzh_charcnt_len;
let tzh_timecnt_data: Vec<i64> = buffer[HEADER_LEN + header.v2_header_start
..HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8]
.chunks_exact(8)
.map(|tt| BE::read_i64(tt))
.collect();
let tzh_timecnt_indices: &[u8] =
&buffer[HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8..tzh_timecnt_end];
let abbrs = from_utf8(&buffer[tzh_leapcnt_end..tzh_charcnt_end]).unwrap();
let tzh_typecnt: Vec<Ttinfo> = buffer[tzh_timecnt_end..tzh_typecnt_end]
.chunks_exact(6)
.map(|tti| {
let offset = tti[5];
let index = abbrs
.chars()
.take(offset as usize)
.filter(|x| *x == '\0')
.count();
Ttinfo {
tt_utoff: BE::read_i32(&tti[0..4]) as isize,
tt_isdst: tti[4],
tt_abbrind: index as u8,
}
})
.collect();
let mut tz_abbr: Vec<String> = abbrs.split("\u{0}").map(|st| st.to_string()).collect();
if tz_abbr.pop().is_none() {
return Err(TzError::EmptyString);
};
Ok(Tz {
tzh_timecnt_data,
tzh_timecnt_indices: tzh_timecnt_indices.to_vec(),
tzh_typecnt,
tz_abbr,
})
}
#[cfg(feature = "std")]
fn parse_data(buffer: &[u8], header: Header, filename: &str) -> Result<Tz, TzError> {
let tzh_timecnt_len: usize = header.tzh_timecnt * 9;
let tzh_typecnt_len: usize = header.tzh_typecnt * 6;
let tzh_leapcnt_len: usize = header.tzh_leapcnt * 12;
let tzh_charcnt_len: usize = header.tzh_charcnt;
let tzh_timecnt_end: usize = HEADER_LEN + header.v2_header_start + tzh_timecnt_len;
let tzh_typecnt_end: usize = tzh_timecnt_end + tzh_typecnt_len;
let tzh_leapcnt_end: usize = tzh_typecnt_end + tzh_leapcnt_len;
let tzh_charcnt_end: usize = tzh_leapcnt_end + tzh_charcnt_len;
let tzh_timecnt_data: Vec<i64> = buffer[HEADER_LEN + header.v2_header_start
..HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8]
.chunks_exact(8)
.map(BE::read_i64)
.collect();
let tzh_timecnt_indices: &[u8] =
&buffer[HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8..tzh_timecnt_end];
let abbrs = from_utf8(&buffer[tzh_leapcnt_end..tzh_charcnt_end])?;
let tzh_typecnt: Vec<Ttinfo> = buffer[tzh_timecnt_end..tzh_typecnt_end]
.chunks_exact(6)
.map(|tti| {
let offset = tti[5];
let index = abbrs
.chars()
.take(offset as usize)
.filter(|x| *x == '\0')
.count();
Ttinfo {
tt_utoff: BE::read_i32(&tti[0..4]) as isize,
tt_isdst: tti[4],
tt_abbrind: index as u8,
}
})
.collect();
let mut tz_abbr: Vec<String> = abbrs.split('\u{0}').map(|st| st.to_string()).collect();
if tz_abbr.pop().is_none() {
return Err(TzError::EmptyString);
};
let mut timezone = String::new();
#[cfg(not(windows))]
let mut tz: Vec<&str> = filename.split('/').collect();
#[cfg(windows)]
let mut tz: Vec<&str> = filename.split("\\").collect();
if tz.len() < 3 {
return Err(TzError::InvalidTimezone);
}
for _ in 0..(tz.len()) - 2 {
tz.remove(0);
}
if tz[0] != "zoneinfo" {
timezone.push_str(tz[0]);
timezone.push('/');
}
timezone.push_str(tz[1]);
#[cfg(any(feature = "parse", feature = "json"))]
{
return Ok(Tz {
tzh_timecnt_data,
tzh_timecnt_indices: tzh_timecnt_indices.to_vec(),
tzh_typecnt,
tz_abbr,
name: timezone,
});
}
#[cfg(not(any(feature = "parse", feature = "json")))]
Ok(Tz {
tzh_timecnt_data,
tzh_timecnt_indices: tzh_timecnt_indices.to_vec(),
tzh_typecnt,
tz_abbr,
})
}
#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
fn read(tz: &str) -> Result<Vec<u8>, std::io::Error> {
let mut f = File::open(tz)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
Ok(buffer)
}
#[cfg(any(feature = "parse", feature = "json"))]
pub fn transition_times(&self, y: Option<i32>) -> Result<Vec<TransitionTime>, TzError> {
let timezone = self;
if timezone.tzh_timecnt_data.len() == 0 {
return Err(TzError::NoData);
}
let mut timechanges = Vec::new();
let mut nearest_timechange: usize = 0;
let mut parsedtimechanges = Vec::new();
if y.is_some() {
let d = Utc::now();
let y = y.unwrap();
let y = if y == 0 {
d.format("%Y").to_string().parse()?
} else {
y
};
let yearbeg = Utc.with_ymd_and_hms(y, 1, 1, 0, 0, 0).unwrap().timestamp();
let yearend = Utc
.with_ymd_and_hms(y, 12, 31, 0, 0, 0)
.unwrap()
.timestamp();
for t in 0..timezone.tzh_timecnt_data.len() {
if timezone.tzh_timecnt_data[t] > yearbeg && timezone.tzh_timecnt_data[t] < yearend
{
timechanges.push(t);
}
if timezone.tzh_timecnt_data[t] < yearbeg {
nearest_timechange = t;
};
}
} else {
for t in 0..timezone.tzh_timecnt_data.len() {
if timezone.tzh_timecnt_data[t] != -576460752303423488 {
timechanges.push(t)
};
}
}
if timechanges.len() != 0 {
for t in 0..timechanges.len() {
let tc = TransitionTime {
time: Utc
.timestamp_opt(timezone.tzh_timecnt_data[timechanges[t]], 0)
.unwrap(),
utc_offset: timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[timechanges[t]] as usize]
.tt_utoff,
isdst: timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[timechanges[t]] as usize]
.tt_isdst
== 1,
abbreviation: timezone.tz_abbr[timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[timechanges[t]] as usize]
.tt_abbrind as usize]
.to_string(),
};
parsedtimechanges.push(tc);
}
} else {
let tc = TransitionTime {
time: Utc
.timestamp_opt(timezone.tzh_timecnt_data[nearest_timechange], 0)
.unwrap(),
utc_offset: timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[nearest_timechange] as usize]
.tt_utoff,
isdst: timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[nearest_timechange] as usize]
.tt_isdst
== 1,
abbreviation: timezone.tz_abbr[timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[nearest_timechange] as usize]
.tt_abbrind as usize]
.to_string(),
};
parsedtimechanges.push(tc);
}
Ok(parsedtimechanges)
}
#[cfg(any(feature = "parse", feature = "json"))]
pub fn zoneinfo(&self) -> Result<Tzinfo, TzError> {
let parsedtimechanges = match self.transition_times(Some(0)) {
Ok(p) => p,
Err(TzError::NoData) => Vec::new(),
Err(e) => return Err(e),
};
let d = Utc::now();
if parsedtimechanges.len() == 2 {
let dst = d > parsedtimechanges[0].time && d < parsedtimechanges[1].time;
let utc_offset = if dst == true {
FixedOffset::east_opt(parsedtimechanges[0].utc_offset as i32).unwrap()
} else {
FixedOffset::east_opt(parsedtimechanges[1].utc_offset as i32).unwrap()
};
Ok(Tzinfo {
timezone: (self.name).clone(),
week_number: d
.with_timezone(&utc_offset)
.format("%V")
.to_string()
.parse()?,
utc_datetime: d,
datetime: d.with_timezone(&utc_offset),
dst_from: Some(parsedtimechanges[0].time),
dst_until: Some(parsedtimechanges[1].time),
dst_period: dst,
raw_offset: parsedtimechanges[1].utc_offset,
dst_offset: parsedtimechanges[0].utc_offset,
utc_offset: utc_offset,
abbreviation: if dst == true {
parsedtimechanges[0].abbreviation.clone()
} else {
parsedtimechanges[1].abbreviation.clone()
},
})
} else if parsedtimechanges.len() == 1 {
let utc_offset = FixedOffset::east_opt(parsedtimechanges[0].utc_offset as i32).unwrap();
Ok(Tzinfo {
timezone: (self.name).clone(),
week_number: d
.with_timezone(&utc_offset)
.format("%V")
.to_string()
.parse()?,
utc_datetime: d,
datetime: d.with_timezone(&utc_offset),
dst_from: None,
dst_until: None,
dst_period: false,
raw_offset: parsedtimechanges[0].utc_offset,
dst_offset: 0,
utc_offset: utc_offset,
abbreviation: parsedtimechanges[0].abbreviation.clone(),
})
} else if parsedtimechanges.len() == 0 {
let utc_offset = FixedOffset::east_opt(self.tzh_typecnt[0].tt_utoff as i32).unwrap();
Ok(Tzinfo {
timezone: (self.name).clone(),
week_number: d
.with_timezone(&utc_offset)
.format("%V")
.to_string()
.parse()?,
utc_datetime: d,
datetime: d.with_timezone(&utc_offset),
dst_from: None,
dst_until: None,
dst_period: false,
raw_offset: self.tzh_typecnt[0].tt_utoff,
dst_offset: 0,
utc_offset: utc_offset,
abbreviation: (self.name).clone(),
})
} else {
Err(TzError::NoData)
}
}
}