1use std::collections::HashMap;
10use std::path::Path;
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug, Clone, PartialEq)]
14pub struct Zone {
15 pub name: String,
16 transitions: Vec<i64>,
17 indices: Vec<u8>,
19 types: Vec<i32>,
21 footer: Option<Rule>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
26struct Rule {
27 std_offset: i32,
28 dst: Option<DstRule>,
29}
30
31#[derive(Debug, Clone, PartialEq)]
32struct DstRule {
33 offset: i32,
34 start: (DateRule, i32),
35 end: (DateRule, i32),
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
39enum DateRule {
40 MonthWeekDay(u32, u32, u32),
42 Julian1(u32),
44 Julian0(u32),
46}
47
48impl Zone {
49 pub fn utc() -> Zone {
50 Zone {
51 name: "UTC".into(),
52 transitions: vec![],
53 indices: vec![],
54 types: vec![0],
55 footer: None,
56 }
57 }
58
59 pub fn load(dir: &Path, name: &str) -> Result<Zone, String> {
60 if name == "UTC" || name == "Etc/UTC" {
61 return Ok(Zone::utc());
62 }
63 let valid = !name.is_empty()
64 && !name.contains("..")
65 && !name.starts_with('/')
66 && name
67 .chars()
68 .all(|c| c.is_ascii_alphanumeric() || "/_+-".contains(c));
69 if !valid {
70 return Err(format!("invalid time zone name {name:?}"));
71 }
72 let bytes = std::fs::read(dir.join(name)).map_err(|e| format!("time zone {name}: {e}"))?;
73 Zone::parse(name, &bytes)
74 }
75
76 pub fn parse(name: &str, b: &[u8]) -> Result<Zone, String> {
77 let err = |m: &str| format!("time zone {name}: {m}");
78 let header = |b: &[u8]| -> Result<(u8, [usize; 6]), String> {
79 if b.len() < 44 || &b[..4] != b"TZif" {
80 return Err(err("not a TZif file"));
81 }
82 let mut counts = [0usize; 6];
83 for (i, c) in counts.iter_mut().enumerate() {
84 *c = u32::from_be_bytes(b[20 + i * 4..24 + i * 4].try_into().unwrap()) as usize;
85 }
86 Ok((b[4], counts))
87 };
88 let (version, c) = header(b)?;
89 let [isut, isstd, leap, time, typ, chars] = c;
90 let v1_len = 44 + time * 5 + typ * 6 + chars + leap * 8 + isstd + isut;
91 let (data, time_size, counts) = if version >= b'2' {
92 let rest = b.get(v1_len..).ok_or_else(|| err("truncated"))?;
93 let (_, c2) = header(rest)?;
94 (&rest[44..], 8, c2)
95 } else {
96 (&b[44..], 4, c)
97 };
98 let [isut, isstd, leap, time, typ, chars] = counts;
99 let need =
100 time * time_size + time + typ * 6 + chars + leap * (time_size + 4) + isstd + isut;
101 if data.len() < need {
102 return Err(err("truncated data block"));
103 }
104 let mut pos = 0;
105 let mut transitions = Vec::with_capacity(time);
106 for _ in 0..time {
107 let t = if time_size == 8 {
108 i64::from_be_bytes(data[pos..pos + 8].try_into().unwrap())
109 } else {
110 i32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()) as i64
111 };
112 transitions.push(t);
113 pos += time_size;
114 }
115 let indices = data[pos..pos + time].to_vec();
116 pos += time;
117 let mut types = Vec::with_capacity(typ);
118 for _ in 0..typ {
119 types.push(i32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()));
120 pos += 6;
121 }
122 if indices.iter().any(|&i| i as usize >= types.len()) || types.is_empty() {
123 return Err(err("invalid type index"));
124 }
125 pos += chars + leap * (time_size + 4) + isstd + isut;
126 let footer = if version >= b'2' {
127 let rest = &data[pos..];
128 let text = std::str::from_utf8(rest).unwrap_or("").trim_matches('\n');
129 if text.is_empty() {
130 None
131 } else {
132 Some(parse_rule(text).ok_or_else(|| err("invalid footer rule"))?)
133 }
134 } else {
135 None
136 };
137 Ok(Zone {
138 name: name.into(),
139 transitions,
140 indices,
141 types,
142 footer,
143 })
144 }
145
146 pub fn from_posix(s: &str) -> Option<Zone> {
148 let rule = parse_rule(s)?;
149 Some(Zone {
150 name: s.into(),
151 transitions: vec![],
152 indices: vec![],
153 types: vec![rule.std_offset],
154 footer: Some(rule),
155 })
156 }
157
158 pub fn offset_at(&self, t: i64) -> i32 {
160 match self.transitions.partition_point(|&x| x <= t) {
161 0 if !self.transitions.is_empty() => self.types[0],
162 i if i == self.transitions.len() => match &self.footer {
163 Some(rule) => rule.offset_at(t),
164 None => self
165 .indices
166 .last()
167 .map(|&k| self.types[k as usize])
168 .unwrap_or(self.types[0]),
169 },
170 i => self.types[self.indices[i - 1] as usize],
171 }
172 }
173
174 pub fn next_transition_after(&self, t: i64) -> Option<i64> {
176 let i = self.transitions.partition_point(|&x| x <= t);
177 if i < self.transitions.len() {
178 return Some(self.transitions[i]);
179 }
180 let rule = self.footer.as_ref()?;
181 let dst = rule.dst.as_ref()?;
182 let (year, ..) = ocpp_core::DateTime::from_unix_seconds(t).to_utc_fields();
183 let mut candidates: Vec<i64> = Vec::new();
184 for y in year - 1..=year + 2 {
185 candidates
186 .push(dst.start.0.local_seconds(y) + dst.start.1 as i64 - rule.std_offset as i64);
187 candidates.push(dst.end.0.local_seconds(y) + dst.end.1 as i64 - dst.offset as i64);
188 }
189 candidates.into_iter().filter(|&c| c > t).min()
190 }
191}
192
193impl Rule {
194 fn offset_at(&self, t: i64) -> i32 {
195 let Some(dst) = &self.dst else {
196 return self.std_offset;
197 };
198 let (year, ..) =
199 ocpp_core::DateTime::from_unix_seconds(t + self.std_offset as i64).to_utc_fields();
200 let start = dst.start.0.local_seconds(year) + dst.start.1 as i64 - self.std_offset as i64;
201 let end = dst.end.0.local_seconds(year) + dst.end.1 as i64 - dst.offset as i64;
202 let in_dst = if start < end {
203 t >= start && t < end
204 } else {
205 !(t >= end && t < start)
206 };
207 if in_dst { dst.offset } else { self.std_offset }
208 }
209}
210
211impl DateRule {
212 fn local_seconds(self, year: i64) -> i64 {
214 let day = |m: u32, d: u32| {
215 ocpp_core::DateTime::from_utc_fields(year, m, d, 0, 0, 0, 0)
216 .map(|t| t.unix_seconds())
217 .unwrap_or(0)
218 };
219 match self {
220 DateRule::MonthWeekDay(m, w, d) => {
221 let first = day(m, 1);
222 let first_wd = ((first.div_euclid(86_400) + 4).rem_euclid(7)) as u32; let mut dom = 1 + (d + 7 - first_wd) % 7 + (w.saturating_sub(1)) * 7;
224 while dom > days_in_month(year, m) {
225 dom -= 7;
226 }
227 first + (dom as i64 - 1) * 86_400
228 }
229 DateRule::Julian1(n) => {
230 let leap = leap_extra(year) > 0;
231 let n = if leap && n >= 60 { n } else { n - 1 };
232 day(1, 1) + n as i64 * 86_400
233 }
234 DateRule::Julian0(n) => day(1, 1) + n as i64 * 86_400,
235 }
236 }
237}
238
239fn days_in_month(year: i64, m: u32) -> u32 {
240 match m {
241 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
242 4 | 6 | 9 | 11 => 30,
243 _ if leap_extra(year) > 0 => 29,
244 _ => 28,
245 }
246}
247
248fn leap_extra(year: i64) -> i64 {
249 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
250 86_400
251 } else {
252 0
253 }
254}
255
256fn parse_rule(s: &str) -> Option<Rule> {
257 let mut p = Parser {
258 s: s.as_bytes(),
259 i: 0,
260 };
261 p.name()?;
262 let std_offset = -p.offset()?;
263 if p.done() {
264 return Some(Rule {
265 std_offset,
266 dst: None,
267 });
268 }
269 p.name()?;
270 let dst_offset = if p.peek().is_some_and(|c| c != b',') {
271 -p.offset()?
272 } else {
273 std_offset + 3600
274 };
275 if p.done() {
276 return Some(Rule {
278 std_offset,
279 dst: Some(DstRule {
280 offset: dst_offset,
281 start: (DateRule::MonthWeekDay(3, 2, 0), 7200),
282 end: (DateRule::MonthWeekDay(11, 1, 0), 7200),
283 }),
284 });
285 }
286 p.expect(b',')?;
287 let start = p.date_time()?;
288 p.expect(b',')?;
289 let end = p.date_time()?;
290 p.done().then_some(Rule {
291 std_offset,
292 dst: Some(DstRule {
293 offset: dst_offset,
294 start,
295 end,
296 }),
297 })
298}
299
300struct Parser<'a> {
301 s: &'a [u8],
302 i: usize,
303}
304
305impl Parser<'_> {
306 fn peek(&self) -> Option<u8> {
307 self.s.get(self.i).copied()
308 }
309 fn done(&self) -> bool {
310 self.i >= self.s.len()
311 }
312 fn expect(&mut self, c: u8) -> Option<()> {
313 (self.peek()? == c).then(|| self.i += 1)
314 }
315 fn name(&mut self) -> Option<()> {
316 if self.peek()? == b'<' {
317 let end = self.s[self.i..].iter().position(|&c| c == b'>')?;
318 self.i += end + 1;
319 } else {
320 let start = self.i;
321 while self.peek().is_some_and(|c| c.is_ascii_alphabetic()) {
322 self.i += 1;
323 }
324 if self.i - start < 3 {
325 return None;
326 }
327 }
328 Some(())
329 }
330 fn number(&mut self) -> Option<i64> {
331 let start = self.i;
332 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
333 self.i += 1;
334 }
335 std::str::from_utf8(&self.s[start..self.i])
336 .ok()?
337 .parse()
338 .ok()
339 }
340 fn offset(&mut self) -> Option<i32> {
342 let sign = match self.peek()? {
343 b'-' => {
344 self.i += 1;
345 -1
346 }
347 b'+' => {
348 self.i += 1;
349 1
350 }
351 _ => 1,
352 };
353 let mut secs = self.number()? * 3600;
354 if self.peek() == Some(b':') {
355 self.i += 1;
356 secs += self.number()? * 60;
357 if self.peek() == Some(b':') {
358 self.i += 1;
359 secs += self.number()?;
360 }
361 }
362 Some(sign * secs as i32)
363 }
364 fn date_time(&mut self) -> Option<(DateRule, i32)> {
365 let rule = match self.peek()? {
366 b'M' => {
367 self.i += 1;
368 let m = self.number()? as u32;
369 self.expect(b'.')?;
370 let w = self.number()? as u32;
371 self.expect(b'.')?;
372 let d = self.number()? as u32;
373 ((1..=12).contains(&m) && (1..=5).contains(&w) && d <= 6)
374 .then_some(DateRule::MonthWeekDay(m, w, d))?
375 }
376 b'J' => {
377 self.i += 1;
378 DateRule::Julian1(self.number()? as u32)
379 }
380 _ => DateRule::Julian0(self.number()? as u32),
381 };
382 let time = if self.peek() == Some(b'/') {
383 self.i += 1;
384 self.offset()?
385 } else {
386 7200
387 };
388 Some((rule, time))
389 }
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub struct Local {
395 pub year: i64,
396 pub month: u32,
397 pub day: u32,
398 pub second_of_day: u32,
400 pub weekday: u32,
402}
403
404impl Zone {
405 pub fn local(&self, t: i64) -> Local {
406 let lt = t + self.offset_at(t) as i64;
407 let (year, month, day, h, m, s, _) =
408 ocpp_core::DateTime::from_unix_seconds(lt).to_utc_fields();
409 Local {
410 year,
411 month,
412 day,
413 second_of_day: h * 3600 + m * 60 + s,
414 weekday: ((lt.div_euclid(86_400) + 3).rem_euclid(7)) as u32,
415 }
416 }
417}
418
419#[derive(Default)]
421pub struct Zones {
422 cache: Mutex<HashMap<String, Arc<Zone>>>,
423}
424
425impl Zones {
426 pub fn get(&self, dir: &Path, name: &str) -> Arc<Zone> {
428 if let Some(z) = self
429 .cache
430 .lock()
431 .unwrap_or_else(|e| e.into_inner())
432 .get(name)
433 {
434 return z.clone();
435 }
436 let zone = Arc::new(Zone::load(dir, name).unwrap_or_else(|e| {
437 tracing::warn!(error = %e, "using UTC instead");
438 Zone::utc()
439 }));
440 self.cache
441 .lock()
442 .unwrap_or_else(|e| e.into_inner())
443 .insert(name.to_owned(), zone.clone());
444 zone
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 fn t(s: &str) -> i64 {
453 ocpp_core::DateTime::parse(s).unwrap().unix_seconds()
454 }
455
456 #[test]
457 fn posix_rules() {
458 let paris = Zone::from_posix("CET-1CEST,M3.5.0,M10.5.0/3").unwrap();
459 assert_eq!(paris.offset_at(t("2024-03-31T00:59:59Z")), 3600);
461 assert_eq!(paris.offset_at(t("2024-03-31T01:00:00Z")), 7200);
462 assert_eq!(paris.offset_at(t("2024-10-27T00:59:59Z")), 7200);
463 assert_eq!(paris.offset_at(t("2024-10-27T01:00:00Z")), 3600);
464 assert_eq!(
465 paris.next_transition_after(t("2024-06-01T00:00:00Z")),
466 Some(t("2024-10-27T01:00:00Z"))
467 );
468 let ny = Zone::from_posix("EST5EDT,M3.2.0,M11.1.0").unwrap();
469 assert_eq!(ny.offset_at(t("2024-03-10T06:59:59Z")), -5 * 3600);
470 assert_eq!(ny.offset_at(t("2024-03-10T07:00:00Z")), -4 * 3600);
471 let syd = Zone::from_posix("AEST-10AEDT,M10.1.0,M4.1.0/3").unwrap();
473 assert_eq!(syd.offset_at(t("2024-01-15T00:00:00Z")), 11 * 3600);
474 assert_eq!(syd.offset_at(t("2024-06-15T00:00:00Z")), 10 * 3600);
475 assert_eq!(
476 Zone::from_posix("<+0330>-3:30").unwrap().offset_at(0),
477 3 * 3600 + 1800
478 );
479 let l = paris.local(t("2024-07-01T22:30:00Z"));
480 assert_eq!(
481 (l.month, l.day, l.second_of_day, l.weekday),
482 (7, 2, 1800, 1)
483 ); }
485
486 #[test]
487 fn system_zoneinfo() {
488 let dir = Path::new("/usr/share/zoneinfo");
489 if !dir.join("Europe/Paris").exists() {
490 return;
491 }
492 let paris = Zone::load(dir, "Europe/Paris").unwrap();
493 assert_eq!(paris.offset_at(t("2024-01-15T12:00:00Z")), 3600);
494 assert_eq!(paris.offset_at(t("2024-07-15T12:00:00Z")), 7200);
495 assert_eq!(paris.offset_at(t("2100-07-15T12:00:00Z")), 7200);
497 assert_eq!(paris.offset_at(t("1999-01-01T00:00:00Z")), 3600);
498 assert_eq!(paris.offset_at(t("1999-08-01T00:00:00Z")), 7200);
499 assert!(Zone::load(dir, "../etc/passwd").is_err());
500 }
501}