use byteorder::{ByteOrder, BE};
#[cfg(feature = "with-chrono")]
use chrono::prelude::*;
use dirs;
use std::{env, error, fmt, fs::File, io::prelude::*, path::PathBuf, str::from_utf8};
static MAGIC: u32 = 0x545A6966;
static V1_HEADER_END: usize = 0x2C;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TzError {
InvalidTimezone,
InvalidMagic,
BadUtf8String,
}
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"
})
}
}
impl From<std::io::Error> for TzError {
fn from(_e: std::io::Error) -> TzError {
TzError::InvalidTimezone
}
}
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<i32>,
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_leapcnt: usize,
tzh_timecnt: usize,
tzh_typecnt: usize,
tzh_charcnt: usize,
}
pub fn parse(tz: &str) -> Result<Tz, TzError> {
let header = parse_header(tz)?;
parse_data(header, tz)
}
fn parse_header(tz: &str) -> Result<Header, TzError> {
let buffer = read(tz)?;
let magic = BE::read_u32(&buffer[0x00..=0x03]);
if magic != MAGIC {
return Err(TzError::InvalidMagic);
}
Ok(Header {
tzh_leapcnt: BE::read_i32(&buffer[0x1C..=0x1F]) as usize,
tzh_timecnt: BE::read_i32(&buffer[0x20..=0x23]) as usize,
tzh_typecnt: BE::read_i32(&buffer[0x24..=0x27]) as usize,
tzh_charcnt: BE::read_i32(&buffer[0x28..=0x2b]) as usize,
})
}
fn parse_data(header: Header, tz: &str) -> Result<Tz, TzError> {
let buffer = read(tz)?;
let tzh_timecnt_len: usize = header.tzh_timecnt * 5;
let tzh_typecnt_len: usize = header.tzh_typecnt * 6;
let tzh_leapcnt_len: usize = header.tzh_leapcnt * 4;
let tzh_charcnt_len: usize = header.tzh_charcnt;
let tzh_timecnt_end: usize = V1_HEADER_END + 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<i32> = buffer
[V1_HEADER_END..V1_HEADER_END + header.tzh_timecnt * 4]
.chunks_exact(4)
.map(|tt| BE::read_i32(tt))
.collect();
#[cfg(feature = "with-chrono")]
let tzh_timecnt_data: Vec<DateTime<Utc>> = buffer
[V1_HEADER_END..V1_HEADER_END + header.tzh_timecnt * 4]
.chunks_exact(4)
.map(|tt| Utc.timestamp(BE::read_i32(tt).into(), 0))
.collect();
let tzh_timecnt_indices: &[u8] =
&buffer[V1_HEADER_END + header.tzh_timecnt * 4..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> {
let mut tz_files_root = if cfg!(windows) && env::var_os("TZFILES_DIR").is_none() {
let mut d = dirs::home_dir().unwrap_or(PathBuf::from("C:\\Users"));
d.push(".zoneinfo");
d
} else {
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 amph = Header { tzh_leapcnt: 0, tzh_timecnt: 11, tzh_typecnt: 4, tzh_charcnt: 16 };
assert_eq!(parse_header("America/Phoenix").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(feature = "with-chrono")]
#[test]
fn parse_timedata() {
let amph: Vec<DateTime<Utc>> = vec![
Utc.ymd(1901, 12, 13).and_hms(20, 45, 52),
Utc.ymd(1918, 3, 31).and_hms(9, 0, 0),
Utc.ymd(1918, 10, 27).and_hms(8, 0, 0),
Utc.ymd(1919, 3, 30).and_hms(9, 0, 0),
Utc.ymd(1919, 10, 26).and_hms(8, 0, 0),
Utc.ymd(1942, 2, 09).and_hms(9, 0, 0),
Utc.ymd(1944, 1, 1).and_hms(6, 1, 0),
Utc.ymd(1944, 4, 1).and_hms(7, 1, 0),
Utc.ymd(1944, 10, 1).and_hms(6, 1, 0),
Utc.ymd(1967, 4, 30).and_hms(9, 0, 0),
Utc.ymd(1967, 10, 29).and_hms(8, 0, 0)];
assert_eq!(parse("America/Phoenix").unwrap().tzh_timecnt_data, amph);
}
#[test]
fn parse_ttgmtoff() {
let amph: [isize; 3] = [-26898, -21600, -25200];
let c: [isize; 3] = [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];
assert_eq!(c, amph);
}
}