bp3d_util/tzif.rs
1// Copyright (c) 2024, BlockProject 3D
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8// * Redistributions of source code must retain the above copyright notice,
9// this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13// * Neither the name of BlockProject 3D nor the names of its contributors
14// may be used to endorse or promote products derived from this software
15// without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29//! A simple TZIF reader module used to get UTC to local offset.
30//!
31//! **See [RFC](https://www.rfc-editor.org/rfc/rfc8536.html)**
32
33use bytesutil::ReadBytes;
34use std::{fmt::Display, io::Read};
35
36/// A series of six-octet records specifying a local time type.
37/// The number of records is specified by the "typecnt" field in the header.
38/// Each record has the following format (the lengths of multi-octet fields are shown in parentheses).
39pub struct LocalTimeTypeRecord {
40 /// A four-octet signed integer specifying the number of seconds to be added to UT in order to determine local time.
41 pub utoff: i32,
42
43 /// A one-octet value indicating whether local time should be considered Daylight Saving Time (DST).
44 pub dst: bool,
45
46 /// A one-octet unsigned integer specifying a zero-based index into the series of time zone designation octets,
47 /// thereby selecting a particular designation string.
48 pub idx: u8,
49}
50
51/// A series of eight- or twelve-octet records specifying the corrections that need to be applied to UTC in order to determine TAI.
52pub struct LeapSecondRecord {
53 /// A four or eight-octet UNIX leap time value specifying the time at which a leap-second correction occurs.
54 pub occurrence: i64,
55
56 /// A four-octet signed integer specifying the value of LEAPCORR on or after the occurrence.
57 pub correction: i32,
58}
59
60/// Possible errors when parsing TZIF.
61#[derive(Debug)]
62pub enum Error {
63 /// An Io error.
64 Io(std::io::Error),
65
66 /// The signature of the file cannot be recognized.
67 InvalidSignature,
68}
69
70impl Display for Error {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Error::Io(e) => write!(f, "io error: {}", e),
74 Error::InvalidSignature => f.write_str("invalid TZIF signature"),
75 }
76 }
77}
78
79impl std::error::Error for Error {}
80
81/// A data block.
82pub struct Data {
83 /// A series of four- or eight-octet UNIX leap-time values sorted in strictly ascending order.
84 pub transition_times: Vec<i64>,
85
86 /// A series of one-octet unsigned integers specifying the type of local time of the corresponding transition time.
87 pub transition_types: Vec<u8>,
88
89 /// A series of six-octet records specifying a local time type.
90 pub local_time_type_records: Vec<LocalTimeTypeRecord>,
91
92 /// A series of eight- or twelve-octet records specifying the corrections that need to be applied to UTC in order to determine TAI.
93 pub leap_second_records: Vec<LeapSecondRecord>,
94}
95
96/// TZIF header.
97pub struct Header {
98 /// Block version.
99 pub version: u8, //4
100
101 /// A four-octet unsigned integer specifying the number of UT/local indicators contained in the data block -- MUST either be zero or equal to "typecnt".
102 pub isutcnt: u32, //20..24
103
104 /// A four-octet unsigned integer specifying the number of standard/wall indicators contained in the data block -- MUST either be zero or equal to "typecnt".
105 pub isstdcnt: u32, //24..28
106
107 /// A four-octet unsigned integer specifying the number of leap-second records contained in the data block.
108 pub leapcnt: u32, //28..32
109
110 /// A four-octet unsigned integer specifying the number of transition times contained in the data block.
111 pub timecnt: u32, //32..36
112
113 /// A four-octet unsigned integer specifying the number of local time type records contained in the data block -- MUST NOT be zero.
114 pub typecnt: u32, //36..40
115
116 /// A four-octet unsigned integer specifying the total number of octets used by the set of time zone designations contained in the data block - MUST NOT be zero.
117 pub charcnt: u32, //40..44
118}
119
120/// Combined header and data block.
121///
122/// According to RFC, there can be currently 2 blocks: 1 for V1 header/data and 1 for V2+ header/data.
123pub struct Block {
124 /// The header block.
125 pub header: Header,
126
127 /// The associated data block.
128 pub data: Data,
129}
130
131/// Simplified TZIF decoded structure.
132pub struct TZIF {
133 /// The V1 combined header/data block.
134 pub block_v1: Block,
135
136 /// The V2+ combined header/data block.
137 pub block_v2p: Option<Block>,
138}
139
140impl Header {
141 fn time_size(&self) -> usize {
142 match self.version {
143 0x00 => 4,
144 _ => 8,
145 }
146 }
147
148 fn read<R: Read>(mut reader: R) -> Result<Header, Error> {
149 let mut header: [u8; 44] = [0; 44];
150 reader.read_exact(&mut header).map_err(Error::Io)?;
151 if &header[0..4] != b"TZif" {
152 return Err(Error::InvalidSignature);
153 }
154 Ok(Header {
155 version: header[4],
156 isutcnt: u32::read_bytes_be(&header[20..24]),
157 isstdcnt: u32::read_bytes_be(&header[24..28]),
158 leapcnt: u32::read_bytes_be(&header[28..32]),
159 timecnt: u32::read_bytes_be(&header[32..36]),
160 typecnt: u32::read_bytes_be(&header[36..40]),
161 charcnt: u32::read_bytes_be(&header[40..44]),
162 })
163 }
164}
165
166impl Data {
167 fn read<R: Read>(mut reader: R, header: &Header) -> Result<Data, Error> {
168 let size = header.time_size();
169 let mut transition_times = vec![0; size * header.timecnt as usize];
170 reader
171 .read_exact(&mut transition_times)
172 .map_err(Error::Io)?;
173 let mut transition_types = vec![0; header.timecnt as usize];
174 reader
175 .read_exact(&mut transition_types)
176 .map_err(Error::Io)?;
177 let mut local_time_type_records = vec![0; 6 * header.typecnt as usize];
178 reader
179 .read_exact(&mut local_time_type_records)
180 .map_err(Error::Io)?;
181 let mut time_zone_designations = vec![0; header.charcnt as usize];
182 reader
183 .read_exact(&mut time_zone_designations)
184 .map_err(Error::Io)?;
185 let mut leap_second_records = vec![0; (size + 4) * header.leapcnt as usize];
186 reader
187 .read_exact(&mut leap_second_records)
188 .map_err(Error::Io)?;
189 let mut std_wall_indicators = vec![0; header.isstdcnt as usize];
190 reader
191 .read_exact(&mut std_wall_indicators)
192 .map_err(Error::Io)?;
193 let mut ut_indicators = vec![0; header.isutcnt as usize];
194 reader.read_exact(&mut ut_indicators).map_err(Error::Io)?;
195 let local_time_type_records = local_time_type_records
196 .as_slice()
197 .chunks(6)
198 .map(|v| LocalTimeTypeRecord {
199 utoff: i32::read_bytes_be(&v[0..4]),
200 dst: v[4] == 1,
201 idx: v[5],
202 })
203 .collect();
204 if header.version == 0x00 {
205 Ok(Data {
206 transition_types,
207 transition_times: transition_times
208 .as_slice()
209 .chunks(4)
210 .map(|v| i32::read_bytes_be(v) as i64)
211 .collect(),
212 leap_second_records: leap_second_records
213 .as_slice()
214 .chunks(8)
215 .map(|v| LeapSecondRecord {
216 occurrence: i32::read_bytes_be(&v[0..4]) as i64,
217 correction: i32::read_bytes_be(&v[4..8]),
218 })
219 .collect(),
220 local_time_type_records,
221 })
222 } else {
223 Ok(Data {
224 transition_types,
225 transition_times: transition_times
226 .as_slice()
227 .chunks(8)
228 .map(i64::read_bytes_be)
229 .collect(),
230 leap_second_records: leap_second_records
231 .as_slice()
232 .chunks(12)
233 .map(|v| LeapSecondRecord {
234 occurrence: i64::read_bytes_be(&v[0..8]),
235 correction: i32::read_bytes_be(&v[8..12]),
236 })
237 .collect(),
238 local_time_type_records,
239 })
240 }
241 }
242}
243
244impl TZIF {
245 /// Reads and decodes a TZIF stream.
246 ///
247 /// # Arguments
248 ///
249 /// * `reader`: the [Read](Read) to read and decode from.
250 ///
251 /// # Errors
252 ///
253 /// This function returns an [Error](Error) if the simplified TZIF structure could not be decoded.
254 pub fn read<R: Read>(mut reader: R) -> Result<TZIF, Error> {
255 let mut header_v1 = Header::read(&mut reader)?;
256 header_v1.version = 0x00; //RFC is badly broken it says bullshit.
257 let block_v1 = Block {
258 data: Data::read(&mut reader, &header_v1)?,
259 header: header_v1,
260 };
261 let block_v2p = match Header::read(&mut reader) {
262 Ok(header_v2) => Some(Block {
263 data: Data::read(&mut reader, &header_v2)?,
264 header: header_v2,
265 }),
266 _ => None,
267 };
268 Ok(TZIF {
269 block_v1,
270 block_v2p,
271 })
272 }
273}