1use alloc::{
2 string::{String, ToString},
3 vec,
4 vec::Vec,
5};
6
7use std::{
8 ffi::OsString,
9 fs::File,
10 path::{Path, PathBuf},
11 sync::{Arc, RwLock},
12 time::Duration,
13};
14
15use crate::{
16 error::{err, Error},
17 timestamp::Timestamp,
18 tz::{
19 concatenated::ConcatenatedTzif, db::special_time_zone, TimeZone,
20 TimeZoneNameIter,
21 },
22 util::{self, array_str::ArrayStr, cache::Expiration, utf8},
23};
24
25const DEFAULT_TTL: Duration = Duration::new(5 * 60, 0);
26
27static TZDATA_LOCATIONS: &[TzdataLocation] = &[
29 TzdataLocation::Env {
30 name: "ANDROID_ROOT",
31 default: "/system",
32 suffix: "usr/share/zoneinfo/tzdata",
33 },
34 TzdataLocation::Env {
35 name: "ANDROID_DATA",
36 default: "/data/misc",
37 suffix: "zoneinfo/current/tzdata",
38 },
39];
40
41pub(crate) struct Database {
42 path: Option<PathBuf>,
43 names: Option<Names>,
44 zones: RwLock<CachedZones>,
45}
46
47impl Database {
48 pub(crate) fn from_env() -> Database {
49 let mut attempted = vec![];
50 for loc in TZDATA_LOCATIONS {
51 let path = loc.to_path_buf();
52 trace!(
53 "opening concatenated tzdata database at {}",
54 path.display()
55 );
56 match Database::from_path(&path) {
57 Ok(db) => return db,
58 Err(_err) => {
59 trace!("failed opening {}: {_err}", path.display());
60 }
61 }
62 attempted.push(path.to_string_lossy().into_owned());
63 }
64 debug!(
65 "could not find concatenated tzdata database at any of the \
66 following paths: {}",
67 attempted.join(", "),
68 );
69 Database::none()
70 }
71
72 pub(crate) fn from_path(path: &Path) -> Result<Database, Error> {
73 let names = Some(Names::new(path)?);
74 let zones = RwLock::new(CachedZones::new());
75 Ok(Database { path: Some(path.to_path_buf()), names, zones })
76 }
77
78 pub(crate) fn none() -> Database {
80 let path = None;
81 let names = None;
82 let zones = RwLock::new(CachedZones::new());
83 Database { path, names, zones }
84 }
85
86 pub(crate) fn reset(&self) {
87 let mut zones = self.zones.write().unwrap();
88 if let Some(ref names) = self.names {
89 names.reset();
90 }
91 zones.reset();
92 }
93
94 pub(crate) fn get(&self, query: &str) -> Option<TimeZone> {
95 if let Some(tz) = special_time_zone(query) {
96 return Some(tz);
97 }
98 let path = self.path.as_ref()?;
99 {
102 let zones = self.zones.read().unwrap();
103 if let Some(czone) = zones.get(query) {
104 if !czone.is_expired() {
105 trace!(
106 "for time zone query `{query}`, \
107 found cached zone `{}` \
108 (expiration={}, last_modified={:?})",
109 czone.tz.diagnostic_name(),
110 czone.expiration,
111 czone.last_modified,
112 );
113 return Some(czone.tz.clone());
114 }
115 }
116 }
117 let mut zones = self.zones.write().unwrap();
137 let ttl = zones.ttl;
138 match zones.get_zone_index(query) {
139 Ok(i) => {
140 let czone = &mut zones.zones[i];
141 if czone.revalidate(path, ttl) {
142 return Some(czone.tz.clone());
145 }
146 let (scratch1, scratch2) = zones.scratch();
148 let czone = match CachedTimeZone::new(
149 path, query, ttl, scratch1, scratch2,
150 ) {
151 Ok(Some(czone)) => czone,
152 Ok(None) => return None,
153 Err(_err) => {
154 warn!(
155 "failed to re-cache time zone {query} \
156 from {path}: {_err}",
157 path = path.display(),
158 );
159 return None;
160 }
161 };
162 let tz = czone.tz.clone();
163 zones.zones[i] = czone;
164 Some(tz)
165 }
166 Err(i) => {
167 let (scratch1, scratch2) = zones.scratch();
168 let czone = match CachedTimeZone::new(
169 path, query, ttl, scratch1, scratch2,
170 ) {
171 Ok(Some(czone)) => czone,
172 Ok(None) => return None,
173 Err(_err) => {
174 warn!(
175 "failed to cache time zone {query} \
176 from {path}: {_err}",
177 path = path.display(),
178 );
179 return None;
180 }
181 };
182 let tz = czone.tz.clone();
183 zones.zones.insert(i, czone);
184 Some(tz)
185 }
186 }
187 }
188
189 pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
190 let Some(path) = self.path.as_ref() else {
191 return TimeZoneNameIter::empty();
192 };
193 let Some(names) = self.names.as_ref() else {
194 return TimeZoneNameIter::empty();
195 };
196 TimeZoneNameIter::from_iter(names.available(path).into_iter())
197 }
198
199 pub(crate) fn is_definitively_empty(&self) -> bool {
200 self.names.is_none()
201 }
202}
203
204impl core::fmt::Debug for Database {
205 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
206 write!(f, "Concatenated(")?;
207 if let Some(ref path) = self.path {
208 write!(f, "{}", path.display())?;
209 } else {
210 write!(f, "unavailable")?;
211 }
212 write!(f, ")")
213 }
214}
215
216#[derive(Debug)]
217struct CachedZones {
218 zones: Vec<CachedTimeZone>,
219 ttl: Duration,
220 scratch1: Vec<u8>,
221 scratch2: Vec<u8>,
222}
223
224impl CachedZones {
225 const DEFAULT_TTL: Duration = DEFAULT_TTL;
226
227 fn new() -> CachedZones {
228 CachedZones {
229 zones: vec![],
230 ttl: CachedZones::DEFAULT_TTL,
231 scratch1: vec![],
232 scratch2: vec![],
233 }
234 }
235
236 fn get(&self, query: &str) -> Option<&CachedTimeZone> {
237 self.get_zone_index(query).ok().map(|i| &self.zones[i])
238 }
239
240 fn get_zone_index(&self, query: &str) -> Result<usize, usize> {
241 self.zones.binary_search_by(|zone| {
242 utf8::cmp_ignore_ascii_case(zone.name(), query)
243 })
244 }
245
246 fn reset(&mut self) {
247 self.zones.clear();
248 }
249
250 fn scratch(&mut self) -> (&mut Vec<u8>, &mut Vec<u8>) {
251 (&mut self.scratch1, &mut self.scratch2)
252 }
253}
254
255#[derive(Clone, Debug)]
256struct CachedTimeZone {
257 tz: TimeZone,
258 expiration: Expiration,
259 last_modified: Option<Timestamp>,
260}
261
262impl CachedTimeZone {
263 fn new(
276 path: &Path,
277 query: &str,
278 ttl: Duration,
279 scratch1: &mut Vec<u8>,
280 scratch2: &mut Vec<u8>,
281 ) -> Result<Option<CachedTimeZone>, Error> {
282 let file = File::open(path).map_err(|e| Error::io(e).path(path))?;
283 let db = ConcatenatedTzif::open(&file)?;
284 let Some(tz) = db.get(query, scratch1, scratch2)? else {
285 return Ok(None);
286 };
287 let last_modified = util::fs::last_modified_from_file(path, &file);
288 let expiration = Expiration::after(ttl);
289 Ok(Some(CachedTimeZone { tz, expiration, last_modified }))
290 }
291
292 fn is_expired(&self) -> bool {
295 self.expiration.is_expired()
296 }
297
298 fn name(&self) -> &str {
300 self.tz.iana_name().unwrap()
303 }
304
305 fn revalidate(&mut self, path: &Path, ttl: Duration) -> bool {
319 let Some(old_last_modified) = self.last_modified else {
323 trace!(
324 "revalidation for {name} in {path} failed because \
325 old last modified time is unavailable",
326 name = self.name(),
327 path = path.display(),
328 );
329 return false;
330 };
331 let Some(new_last_modified) = util::fs::last_modified_from_path(path)
332 else {
333 trace!(
334 "revalidation for {name} in {path} failed because \
335 new last modified time is unavailable",
336 name = self.name(),
337 path = path.display(),
338 );
339 return false;
340 };
341 if old_last_modified != new_last_modified {
343 trace!(
344 "revalidation for {name} in {path} failed because \
345 last modified times do not match: old = {old} != {new} = new",
346 name = self.name(),
347 path = path.display(),
348 old = old_last_modified,
349 new = new_last_modified,
350 );
351 return false;
352 }
353 trace!(
354 "revalidation for {name} in {path} succeeded because \
355 last modified times match: old = {old} == {new} = new",
356 name = self.name(),
357 path = path.display(),
358 old = old_last_modified,
359 new = new_last_modified,
360 );
361 self.expiration = Expiration::after(ttl);
362 true
363 }
364}
365
366#[derive(Debug)]
381struct Names {
382 inner: RwLock<NamesInner>,
383}
384
385#[derive(Debug)]
386struct NamesInner {
387 names: Vec<Arc<str>>,
389 version: ArrayStr<5>,
391 scratch: Vec<u8>,
394 ttl: Duration,
399 expiration: Expiration,
401}
402
403impl Names {
404 const DEFAULT_TTL: Duration = DEFAULT_TTL;
407
408 fn new(path: &Path) -> Result<Names, Error> {
414 let path = path.to_path_buf();
415 let mut scratch = vec![];
416 let (names, version) = read_names_and_version(&path, &mut scratch)?;
417 trace!(
418 "found concatenated tzdata at {path} \
419 with version {version} and {len} \
420 IANA time zone identifiers",
421 path = path.display(),
422 len = names.len(),
423 );
424 let ttl = Names::DEFAULT_TTL;
425 let expiration = Expiration::after(ttl);
426 let inner = NamesInner { names, version, scratch, ttl, expiration };
427 Ok(Names { inner: RwLock::new(inner) })
428 }
429
430 fn available(&self, path: &Path) -> Vec<String> {
433 let mut inner = self.inner.write().unwrap();
434 inner.attempt_refresh(path);
435 inner.available()
436 }
437
438 fn reset(&self) {
439 self.inner.write().unwrap().reset();
440 }
441}
442
443impl NamesInner {
444 fn available(&self) -> Vec<String> {
446 self.names.iter().map(|name| name.to_string()).collect()
447 }
448
449 fn attempt_refresh(&mut self, path: &Path) {
456 if self.expiration.is_expired() {
457 self.refresh(path);
458 }
459 }
460
461 fn refresh(&mut self, path: &Path) {
466 let result = read_names_and_version(path, &mut self.scratch);
469 self.expiration = Expiration::after(self.ttl);
470 match result {
471 Ok((names, version)) => {
472 trace!(
473 "refreshed concatenated tzdata at {path} \
474 with version {version} and {len} \
475 IANA time zone identifiers",
476 path = path.display(),
477 len = names.len(),
478 );
479 self.names = names;
480 self.version = version;
481 }
482 Err(_err) => {
483 warn!(
484 "failed to refresh concatenated time zone name cache \
485 for {path}: {_err}",
486 path = path.display(),
487 )
488 }
489 }
490 }
491
492 fn reset(&mut self) {
495 self.names.clear();
497 self.expiration = Expiration::expired();
499 }
500}
501
502#[derive(Debug)]
508enum TzdataLocation {
509 Env { name: &'static str, default: &'static str, suffix: &'static str },
510}
511
512impl TzdataLocation {
513 fn to_path_buf(&self) -> PathBuf {
516 match *self {
517 TzdataLocation::Env { name, default, suffix } => {
518 let var = std::env::var_os(name)
519 .unwrap_or_else(|| OsString::from(default));
520 let prefix = PathBuf::from(var);
521 prefix.join(suffix)
522 }
523 }
524 }
525}
526
527fn read_names_and_version(
535 path: &Path,
536 scratch: &mut Vec<u8>,
537) -> Result<(Vec<Arc<str>>, ArrayStr<5>), Error> {
538 let file = File::open(path).map_err(|e| Error::io(e).path(path))?;
539 let db = ConcatenatedTzif::open(file)?;
540 let names: Vec<Arc<str>> =
541 db.available(scratch)?.into_iter().map(Arc::from).collect();
542 if names.is_empty() {
543 return Err(err!(
544 "found no IANA time zone identifiers in \
545 concatenated tzdata file at {path}",
546 path = path.display(),
547 ));
548 }
549 Ok((names, db.version()))
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
563 fn debug_tzdata_list() -> anyhow::Result<()> {
564 let _ = crate::logging::Logger::init();
565
566 const ENV: &str = "JIFF_DEBUG_CONCATENATED_TZDATA";
567 let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
568 let path = PathBuf::from(val);
569 let db = Database::from_path(&path)?;
570 for name in db.available() {
571 std::eprintln!("{name}");
572 }
573 Ok(())
574 }
575}