use byteorder::{ByteOrder, BE};
#[cfg(feature = "with-chrono")]
use chrono::prelude::*;
#[cfg(windows)]
use dirs;
use std::{env, error, fmt, fs::File, io::prelude::*, path::PathBuf, str::from_utf8};
static MAGIC: u32 = 0x545A6966;
static HEADER_LEN: usize = 0x2C;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TzError {
InvalidTimezone,
InvalidMagic,
BadUtf8String,
UnsupportedFormat,
NoData,
ParseError
}
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"
})
}
}
impl From<std::io::Error> for TzError {
fn from(_e: std::io::Error) -> TzError {
TzError::InvalidTimezone
}
}
impl From<std::num::ParseIntError> for TzError {
fn from(_e: std::num::ParseIntError) -> TzError {
TzError::ParseError
}
}
impl From<std::str::Utf8Error> for TzError {
fn from(_e: std::str::Utf8Error) -> TzError {
TzError::BadUtf8String
}
}
impl error::Error for TzError {}
impl From<TzError> for std::io::Error {
fn from(e: TzError) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, e)
}
}
#[cfg(not(feature = "with-chrono"))]
#[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(feature = "with-chrono")]
#[derive(Debug)]
pub struct Tz {
pub tzh_timecnt_data: Vec<DateTime<Utc>>,
pub tzh_timecnt_indices: Vec<u8>,
pub tzh_typecnt: Vec<Ttinfo>,
pub tz_abbr: Vec<String>,
}
#[derive(Debug)]
pub struct Ttinfo {
pub tt_gmtoff: isize,
pub tt_isdst: u8,
pub tt_abbrind: u8,
}
#[derive(Debug, PartialEq)]
struct Header {
tzh_ttisgmtcnt: usize,
tzh_ttisstdcnt: usize,
tzh_leapcnt: usize,
tzh_timecnt: usize,
tzh_typecnt: usize,
tzh_charcnt: usize,
v2_header_start: usize,
}
pub fn parse(tz: &str) -> Result<Tz, TzError> {
let buf = read(tz)?;
let header = parse_header(&buf)?;
parse_data(&buf, header)
}
fn parse_header(buffer: &Vec<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_ttisgmtcnt = 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_ttisgmtcnt +
44;
Ok(Header {
tzh_ttisgmtcnt: 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
})
}
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;
#[cfg(not(feature = "with-chrono"))]
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();
#[cfg(feature = "with-chrono")]
let tzh_timecnt_data: Vec<DateTime<Utc>> = buffer
[HEADER_LEN + header.v2_header_start..HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8]
.chunks_exact(8)
.map(|tt| Utc.timestamp(BE::read_i64(tt).into(), 0))
.collect();
let tzh_timecnt_indices: &[u8] =
&buffer[HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8..tzh_timecnt_end];
let tzh_typecnt: Vec<Ttinfo> = buffer[tzh_timecnt_end..tzh_typecnt_end]
.chunks_exact(6)
.map(|tti| Ttinfo {
tt_gmtoff: BE::read_i32(&tti[0..4]) as isize,
tt_isdst: tti[4],
tt_abbrind: tti[5] / 4,
})
.collect();
let mut tz_abbr: Vec<String> = from_utf8(&buffer[tzh_leapcnt_end..tzh_charcnt_end])?
.split("\u{0}")
.map(|st| st.to_string())
.collect();
tz_abbr.pop().unwrap();
Ok(Tz {
tzh_timecnt_data: tzh_timecnt_data,
tzh_timecnt_indices: tzh_timecnt_indices.to_vec(),
tzh_typecnt: tzh_typecnt,
tz_abbr: tz_abbr,
})
}
fn read(tz: &str) -> Result<Vec<u8>, std::io::Error> {
#[cfg(windows)]
let mut tz_files_root = match env::var_os("TZFILES_DIR") {
Some(tz_dir) => {
let mut d = PathBuf::new();
d.push(tz_dir);
d
},
None => {
let mut d = dirs::home_dir().unwrap_or(PathBuf::from("C:\\Users"));
d.push(".zoneinfo");
d
}
};
#[cfg(not(windows))]
let mut tz_files_root = {
let mut d = PathBuf::new();
d.push(env::var("TZFILES_DIR").unwrap_or(format!("/usr/share/zoneinfo/")));
d
};
tz_files_root.push(tz);
let mut f = File::open(tz_files_root)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
Ok(buffer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_file() {
assert_eq!(read("America/Phoenix").is_ok(), true);
}
#[test]
fn parse_hdr() {
let buf = read("America/Phoenix").unwrap();
let amph = Header { tzh_ttisgmtcnt: 0, tzh_ttisstdcnt: 0, tzh_leapcnt: 0, tzh_timecnt: 11, tzh_typecnt: 4, tzh_charcnt: 16, v2_header_start: 139 };
assert_eq!(parse_header(&buf).unwrap(), amph);
}
#[test]
fn parse_indices() {
let amph: [u8; 11] = [2, 1, 2, 1, 2, 3, 2, 3, 2, 1, 2];
assert_eq!(parse("America/Phoenix").unwrap().tzh_timecnt_indices, amph);
}
#[cfg(not(feature = "with-chrono"))]
#[test]
fn parse_timedata() {
let amph: Vec<i64> = vec![
-2717643600,
-1633273200,
-1615132800,
-1601823600,
-1583683200,
-880210800,
-820519140,
-812653140,
-796845540,
-84380400,
-68659200
];
assert_eq!(parse("America/Phoenix").unwrap().tzh_timecnt_data, amph);
}
#[test]
fn parse_ttgmtoff() {
let amph: [isize; 4] = [-26898, -21600, -25200, -21600];
let c: [isize; 4] = [parse("America/Phoenix").unwrap().tzh_typecnt[0].tt_gmtoff, parse("America/Phoenix").unwrap().tzh_typecnt[1].tt_gmtoff, parse("America/Phoenix").unwrap().tzh_typecnt[2].tt_gmtoff, parse("America/Phoenix").unwrap().tzh_typecnt[3].tt_gmtoff];
assert_eq!(c, amph);
}
}