1use std::collections::HashMap;
11use std::rc::Rc;
12
13#[derive(Clone, Debug, PartialEq)]
15pub struct LocalType {
16 pub offset: i64,
17 pub abbrev: String,
18}
19
20#[derive(Debug)]
22pub struct Tz {
23 pub name: String,
25 transitions: Vec<i64>,
26 type_of: Vec<usize>,
27 types: Vec<LocalType>,
28 rule: Option<PosixRule>,
29}
30
31#[derive(Debug)]
34struct PosixRule {
35 std: LocalType,
36 dst: Option<(LocalType, RuleDate, RuleDate)>,
37}
38
39#[derive(Debug, Clone, Copy)]
43enum RuleDate {
44 MonthWeekDay {
45 month: i64,
46 week: i64,
47 day: i64,
48 time: i64,
49 },
50 Julian1 { day: i64, time: i64 },
52 Julian0 { day: i64, time: i64 },
54}
55
56impl Tz {
57 pub fn at(&self, t: i64) -> LocalType {
59 match self.transitions.binary_search(&t) {
60 Ok(i) => self.types[self.type_of[i]].clone(),
61 Err(i) if i == self.transitions.len() && self.rule.is_some() => {
64 self.rule.as_ref().map(|r| r.at(t)).expect("checked")
65 }
66 Err(0) => self.types[0].clone(),
68 Err(i) => self.types[self.type_of[i - 1]].clone(),
69 }
70 }
71}
72
73thread_local! {
74 static CACHE: std::cell::RefCell<HashMap<String, Option<Rc<Tz>>>> =
75 std::cell::RefCell::new(HashMap::new());
76}
77
78pub fn load(name: &str) -> Option<Rc<Tz>> {
81 if name.is_empty()
82 || name.starts_with('/')
83 || name
84 .split('/')
85 .any(|p| p.is_empty() || p == "." || p == "..")
86 || !name
87 .chars()
88 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '-' | '+'))
89 {
90 return None;
91 }
92 if let Some(hit) = CACHE.with(|c| c.borrow().get(name).cloned()) {
93 return hit;
94 }
95 let tz = [
96 "/usr/share/zoneinfo",
97 "/usr/lib/zoneinfo",
98 "/usr/share/lib/zoneinfo",
99 ]
100 .iter()
101 .find_map(|dir| std::fs::read(format!("{dir}/{name}")).ok())
102 .and_then(|bytes| parse(&bytes, name))
103 .map(Rc::new);
104 CACHE.with(|c| c.borrow_mut().insert(name.to_string(), tz.clone()));
105 tz
106}
107
108fn be32(b: &[u8], at: usize) -> Option<i64> {
109 Some(i32::from_be_bytes(b.get(at..at + 4)?.try_into().ok()?) as i64)
110}
111
112fn be64(b: &[u8], at: usize) -> Option<i64> {
113 Some(i64::from_be_bytes(b.get(at..at + 8)?.try_into().ok()?))
114}
115
116fn counts(b: &[u8], at: usize) -> Option<[usize; 6]> {
119 let mut c = [0usize; 6];
120 for (i, slot) in c.iter_mut().enumerate() {
121 *slot = be32(b, at + 20 + i * 4)? as usize;
122 }
123 Some(c)
124}
125
126fn parse(b: &[u8], name: &str) -> Option<Tz> {
127 if b.get(..4)? != b"TZif" {
128 return None;
129 }
130 let version = *b.get(4)?;
131 let c1 = counts(b, 0)?;
132 let v1_len = c1[3] * 5 + c1[4] * 6 + c1[5] + c1[2] * 8 + c1[1] + c1[0];
133 let (base, c, time_size) = if version >= b'2' {
135 let at = 44 + v1_len;
136 (at, counts(b, at)?, 8)
137 } else {
138 (0, c1, 4)
139 };
140 let [isutcnt, isstdcnt, leapcnt, timecnt, typecnt, charcnt] = c;
141 let mut p = base + 44;
142 let mut transitions = Vec::with_capacity(timecnt);
143 for i in 0..timecnt {
144 transitions.push(if time_size == 8 {
145 be64(b, p + i * 8)?
146 } else {
147 be32(b, p + i * 4)?
148 });
149 }
150 p += timecnt * time_size;
151 let type_of: Vec<usize> = b.get(p..p + timecnt)?.iter().map(|&x| x as usize).collect();
152 p += timecnt;
153 let mut raw_types = Vec::with_capacity(typecnt);
154 for i in 0..typecnt {
155 let at = p + i * 6;
156 raw_types.push((be32(b, at)?, *b.get(at + 5)? as usize));
157 }
158 p += typecnt * 6;
159 let chars = b.get(p..p + charcnt)?;
160 let types: Vec<LocalType> = raw_types
161 .into_iter()
162 .map(|(offset, idx)| {
163 let end = chars[idx.min(chars.len())..]
164 .iter()
165 .position(|&ch| ch == 0)
166 .map_or(chars.len(), |e| idx + e);
167 LocalType {
168 offset,
169 abbrev: String::from_utf8_lossy(&chars[idx.min(end)..end]).into_owned(),
170 }
171 })
172 .collect();
173 if types.is_empty() || type_of.iter().any(|&t| t >= types.len()) {
174 return None;
175 }
176 p += charcnt + leapcnt * (time_size + 4) + isstdcnt + isutcnt;
177 let rule = if version >= b'2' {
178 b.get(p..).and_then(|rest| {
179 let text = std::str::from_utf8(rest).ok()?;
180 let line = text.strip_prefix('\n')?.split('\n').next()?;
181 PosixRule::parse(line)
182 })
183 } else {
184 None
185 };
186 Some(Tz {
187 name: name.to_string(),
188 transitions,
189 type_of,
190 types,
191 rule,
192 })
193}
194
195impl PosixRule {
196 fn parse(s: &str) -> Option<PosixRule> {
197 let mut cur = s;
198 let std_name = take_name(&mut cur)?;
199 let std_off = -take_offset(&mut cur)?;
201 let std = LocalType {
202 offset: std_off,
203 abbrev: std_name,
204 };
205 if cur.is_empty() {
206 return Some(PosixRule { std, dst: None });
207 }
208 let dst_name = take_name(&mut cur)?;
209 let dst_off = if cur.starts_with(',') {
210 std_off + 3600
211 } else {
212 -take_offset(&mut cur)?
213 };
214 let rest = cur.strip_prefix(',')?;
215 let (start, end) = rest.split_once(',')?;
216 Some(PosixRule {
217 std,
218 dst: Some((
219 LocalType {
220 offset: dst_off,
221 abbrev: dst_name,
222 },
223 RuleDate::parse(start)?,
224 RuleDate::parse(end)?,
225 )),
226 })
227 }
228
229 fn at(&self, t: i64) -> LocalType {
230 let Some((dst, start, end)) = &self.dst else {
231 return self.std.clone();
232 };
233 let year = crate::stdlib::date::civil_from_days((t + self.std.offset).div_euclid(86_400)).0;
236 let begin = start.local_secs(year) - self.std.offset;
237 let finish = end.local_secs(year) - dst.offset;
238 let in_dst = if begin < finish {
239 t >= begin && t < finish
240 } else {
241 !(t >= finish && t < begin)
243 };
244 if in_dst {
245 dst.clone()
246 } else {
247 self.std.clone()
248 }
249 }
250}
251
252fn take_name(cur: &mut &str) -> Option<String> {
254 if let Some(rest) = cur.strip_prefix('<') {
255 let end = rest.find('>')?;
256 let name = rest[..end].to_string();
257 *cur = &rest[end + 1..];
258 return Some(name);
259 }
260 let end = cur
261 .find(|c: char| !c.is_ascii_alphabetic())
262 .unwrap_or(cur.len());
263 if end < 3 {
264 return None;
265 }
266 let name = cur[..end].to_string();
267 *cur = &cur[end..];
268 Some(name)
269}
270
271fn take_offset(cur: &mut &str) -> Option<i64> {
273 let (sign, rest) = match cur.as_bytes().first()? {
274 b'-' => (-1, &cur[1..]),
275 b'+' => (1, &cur[1..]),
276 _ => (1, *cur),
277 };
278 let end = rest
279 .find(|c: char| !(c.is_ascii_digit() || c == ':'))
280 .unwrap_or(rest.len());
281 let secs = hms(&rest[..end])?;
282 *cur = &rest[end..];
283 Some(sign * secs)
284}
285
286fn hms(s: &str) -> Option<i64> {
287 let mut total = 0;
288 let mut scale = 3600;
289 for part in s.split(':') {
290 if part.is_empty() || scale == 0 {
291 return None;
292 }
293 total += part.parse::<i64>().ok()? * scale;
294 scale /= 60;
295 }
296 Some(total)
297}
298
299impl RuleDate {
300 fn parse(s: &str) -> Option<RuleDate> {
301 let (date, time) = match s.split_once('/') {
302 Some((d, t)) => {
303 let (neg, t) = match t.strip_prefix('-') {
304 Some(t) => (true, t),
305 None => (false, t.strip_prefix('+').unwrap_or(t)),
306 };
307 let secs = hms(t)?;
308 (d, if neg { -secs } else { secs })
309 }
310 None => (s, 7200),
311 };
312 if let Some(m) = date.strip_prefix('M') {
313 let mut it = m.split('.').map(|x| x.parse::<i64>().ok());
314 let (month, week, day) = (it.next()??, it.next()??, it.next()??);
315 return Some(RuleDate::MonthWeekDay {
316 month,
317 week,
318 day,
319 time,
320 });
321 }
322 if let Some(n) = date.strip_prefix('J') {
323 return Some(RuleDate::Julian1 {
324 day: n.parse().ok()?,
325 time,
326 });
327 }
328 Some(RuleDate::Julian0 {
329 day: date.parse().ok()?,
330 time,
331 })
332 }
333
334 fn local_secs(&self, year: i64) -> i64 {
336 use crate::stdlib::date::days_from_civil;
337 let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
338 let (days, time) = match *self {
339 RuleDate::MonthWeekDay {
340 month,
341 week,
342 day,
343 time,
344 } => {
345 let first = days_from_civil(year, month - 1, 1);
346 let first_wd = (first + 4).rem_euclid(7);
348 let mut d = first + (day - first_wd).rem_euclid(7) + (week - 1) * 7;
349 let next_month = if month == 12 {
350 days_from_civil(year + 1, 0, 1)
351 } else {
352 days_from_civil(year, month, 1)
353 };
354 while d >= next_month {
355 d -= 7;
356 }
357 (d, time)
358 }
359 RuleDate::Julian1 { day, time } => {
360 let skip = if leap && day >= 60 { 1 } else { 0 };
361 (days_from_civil(year, 0, 1) + day - 1 + skip, time)
362 }
363 RuleDate::Julian0 { day, time } => (days_from_civil(year, 0, 1) + day, time),
364 };
365 days * 86_400 + time
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn posix_rule_new_york() {
375 let r = PosixRule::parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
376 assert_eq!(r.at(1_909_137_600).abbrev, "EDT");
378 assert_eq!(r.at(1_893_499_200).abbrev, "EST");
379 let r = PosixRule::parse("<+0530>-5:30").unwrap();
380 assert_eq!(r.at(0).offset, 19_800);
381 assert_eq!(r.at(0).abbrev, "+0530");
382 }
383}