Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/time/system.rs

20.3 KiB, 99 runs

created by r1870400018:6395, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use crate::time::{CalClockZone, tzif::{TZifParser, TZifData}};
5
6use oxedyne_fe2o3_core::prelude::*;
7
8use std::{
9 collections::HashMap,
10 env,
11 fs,
12 path::{Path, PathBuf},
13 sync::{Mutex, OnceLock},
14 time::SystemTime,
15};
16
17#[derive(Clone, Debug)]
18pub struct SystemTimezoneConfig {
19 pub use_system_data: bool,
20 pub require_consent: bool,
21 pub search_paths: Vec<PathBuf>,
22 pub detect_conflicts: bool,
23 pub cache_max_age: u64, // seconds
24}
25
26impl Default for SystemTimezoneConfig {
27 fn default() -> Self {
28 Self {
29 use_system_data: false, // Conservative default
30 require_consent: true,
31 search_paths: Self::default_search_paths(),
32 detect_conflicts: true,
33 cache_max_age: 24 * 60 * 60, // 24 hours
34 }
35 }
36}
37
38impl SystemTimezoneConfig {
39 pub fn automatic() -> Self {
40 Self {
41 use_system_data: true,
42 require_consent: false,
43 detect_conflicts: true,
44 ..Default::default()
45 }
46 }
47
48 pub fn with_consent() -> Self {
49 Self {
50 use_system_data: true,
51 require_consent: true,
52 detect_conflicts: true,
53 ..Default::default()
54 }
55 }
56
57 pub fn default_search_paths() -> Vec<PathBuf> {
58 let mut paths = Vec::new();
59
60 // Unix-like systems
61 if cfg!(unix) {
62 paths.push(PathBuf::from("/usr/share/zoneinfo"));
63 paths.push(PathBuf::from("/usr/lib/zoneinfo"));
64 paths.push(PathBuf::from("/etc/zoneinfo"));
65
66 // macOS
67 if cfg!(target_os = "macos") {
68 paths.push(PathBuf::from("/var/db/timezone/zoneinfo"));
69 }
70 }
71
72 // Windows
73 if cfg!(windows) {
74 if let Ok(windir) = env::var("WINDIR") {
75 paths.push(PathBuf::from(windir).join("System32").join("tzdata"));
76 }
77 }
78
79 // Android
80 if cfg!(target_os = "android") {
81 // Check environment variables for Android paths
82 if let Ok(android_root) = env::var("ANDROID_ROOT") {
83 paths.push(PathBuf::from(android_root).join("usr").join("share").join("zoneinfo"));
84 } else {
85 paths.push(PathBuf::from("/system/usr/share/zoneinfo"));
86 }
87
88 if let Ok(android_data) = env::var("ANDROID_DATA") {
89 paths.push(PathBuf::from(android_data).join("misc").join("zoneinfo"));
90 }
91
92 // Try the concatenated timezone database location
93 paths.push(PathBuf::from("/system/usr/share/zoneinfo/tzdata"));
94 }
95
96 paths
97 }
98
99 pub fn add_search_path(&mut self, path: PathBuf) {
100 self.search_paths.push(path);
101 }
102
103 pub fn remove_search_path(&mut self, path: &Path) {
104 self.search_paths.retain(|p| p != path);
105 }
106}
107
108pub struct SystemTimezoneManager {
109 config: SystemTimezoneConfig,
110 consent_given: Mutex<bool>,
111 cache: Mutex<TimezoneCache>,
112}
113
114#[derive(Debug)]
115struct TimezoneCache {
116 data: HashMap<String, CachedTimezoneData>,
117 last_updated: SystemTime,
118 hit_count: u64,
119 miss_count: u64,
120}
121
122#[derive(Clone, Debug)]
123struct CachedTimezoneData {
124 zone: CalClockZone,
125 #[allow(dead_code)]
126 tzif_data: Option<TZifData>,
127 source_path: PathBuf,
128 last_modified: SystemTime,
129 #[allow(dead_code)]
130 rules_version: String,
131}
132
133static TIMEZONE_MANAGER: OnceLock<SystemTimezoneManager> = OnceLock::new();
134
135impl SystemTimezoneManager {
136 pub fn global() -> &'static Self {
137 TIMEZONE_MANAGER.get_or_init(|| Self::new(SystemTimezoneConfig::default()))
138 }
139
140 pub fn new(config: SystemTimezoneConfig) -> Self {
141 Self {
142 config,
143 consent_given: Mutex::new(false),
144 cache: Mutex::new(TimezoneCache {
145 data: HashMap::new(),
146 last_updated: SystemTime::UNIX_EPOCH,
147 hit_count: 0,
148 miss_count: 0,
149 }),
150 }
151 }
152
153 pub fn configure(config: SystemTimezoneConfig) -> Outcome<()> {
154 if TIMEZONE_MANAGER.get().is_some() {
155 return Err(err!("Timezone manager already initialised"; Invalid, Init));
156 }
157
158 let result = TIMEZONE_MANAGER.set(Self::new(config))
159 .map_err(|_| err!("Failed to set timezone manager"; System));
160 res!(result);
161
162 Ok(())
163 }
164
165 pub fn request_consent(&self) -> Outcome<bool> {
166 if !self.config.require_consent {
167 let mut consent = lock_mutex!(self.consent_given);
168 *consent = true;
169 return Ok(true);
170 }
171
172 // In a real implementation, this would show a user dialog
173 // For now, we'll check an environment variable
174 if env::var("FE2O3_CALCLOCK_TIMEZONE_CONSENT").is_ok() {
175 let mut consent = lock_mutex!(self.consent_given);
176 *consent = true;
177 Ok(true)
178 } else {
179 Ok(false)
180 }
181 }
182
183 pub fn has_consent(&self) -> bool {
184 if !self.config.require_consent {
185 return true;
186 }
187
188 match self.consent_given.lock() {
189 Ok(guard) => *guard,
190 Err(_) => {
191 // If mutex is poisoned, assume no consent for safety
192 eprintln!("Warning: Consent mutex poisoned, assuming no consent");
193 false
194 }
195 }
196 }
197
198 pub fn load_system_timezone(&self, zone_id: &str) -> Outcome<Option<CalClockZone>> {
199 if !self.config.use_system_data {
200 return Ok(None);
201 }
202
203 if self.config.require_consent && !self.has_consent() {
204 if !res!(self.request_consent()) {
205 return Ok(None);
206 }
207 }
208
209 // Check cache first
210 if let Some(cached) = self.get_cached_timezone(zone_id) {
211 if self.is_cache_valid(&cached) {
212 return Ok(Some(cached.zone));
213 }
214 }
215
216 // Try to load from system
217 for search_path in &self.config.search_paths {
218 if let Ok(zone) = self.load_timezone_from_path(search_path, zone_id) {
219 res!(self.cache_timezone(zone_id, zone.clone(), search_path.clone()));
220 return Ok(Some(zone));
221 }
222 }
223
224 Ok(None)
225 }
226
227 fn load_timezone_from_path(&self, base_path: &Path, zone_id: &str) -> Outcome<CalClockZone> {
228 let zone_path = base_path.join(zone_id);
229
230 if !zone_path.exists() {
231 return Err(err!("Timezone file not found: {}", zone_path.display(); NotFound));
232 }
233
234 // Parse TZif file format
235 let mut parser = TZifParser::new();
236 res!(parser.load_from_file(&zone_path));
237
238 if let Some(tzif_data) = parser.timezone_data() {
239 // Create CalClockZone with TZif data
240 CalClockZone::from_tzif_data(zone_id, tzif_data.clone())
241 } else {
242 // Fall back to embedded zone creation
243 CalClockZone::new_embedded(zone_id)
244 }
245 }
246
247 fn get_cached_timezone(&self, zone_id: &str) -> Option<CachedTimezoneData> {
248 let mut cache = match self.cache.lock() {
249 Ok(guard) => guard,
250 Err(_) => {
251 eprintln!("Warning: Cache mutex poisoned in get_cached_timezone");
252 return None;
253 }
254 };
255 if let Some(cached) = cache.data.get(zone_id).cloned() {
256 cache.hit_count += 1;
257 Some(cached)
258 } else {
259 cache.miss_count += 1;
260 None
261 }
262 }
263
264 fn is_cache_valid(&self, cached: &CachedTimezoneData) -> bool {
265 // Check if cache has expired
266 if let Ok(elapsed) = cached.last_modified.elapsed() {
267 if elapsed.as_secs() > self.config.cache_max_age {
268 return false;
269 }
270 }
271
272 // Check if source file has been modified
273 if let Ok(metadata) = fs::metadata(&cached.source_path) {
274 if let Ok(modified) = metadata.modified() {
275 return modified <= cached.last_modified;
276 }
277 }
278
279 true
280 }
281
282 fn cache_timezone(&self, zone_id: &str, zone: CalClockZone, source_path: PathBuf) -> Outcome<()> {
283 let mut cache = lock_mutex!(self.cache);
284
285 let last_modified = if let Ok(metadata) = fs::metadata(&source_path) {
286 metadata.modified().unwrap_or(SystemTime::now())
287 } else {
288 SystemTime::now()
289 };
290
291 // Try to parse TZif data for caching
292 let tzif_data = if source_path.exists() {
293 let mut parser = TZifParser::new();
294 if parser.load_from_file(&source_path).is_ok() {
295 parser.timezone_data().cloned()
296 } else {
297 None
298 }
299 } else {
300 None
301 };
302
303 let rules_version = if let Some(ref tzif) = tzif_data {
304 format!("TZif v{}", tzif.version)
305 } else {
306 "unknown".to_string()
307 };
308
309 let cached_data = CachedTimezoneData {
310 zone,
311 tzif_data,
312 source_path,
313 last_modified,
314 rules_version,
315 };
316
317 cache.data.insert(zone_id.to_string(), cached_data);
318 cache.last_updated = SystemTime::now();
319
320 Ok(())
321 }
322
323 pub fn detect_timezone_conflicts(&self, zone_id: &str, embedded_zone: &CalClockZone) -> Outcome<Vec<TimezoneConflict>> {
324 if !self.config.detect_conflicts {
325 return Ok(Vec::new());
326 }
327
328 let mut conflicts = Vec::new();
329
330 if let Ok(Some(system_zone)) = self.load_system_timezone(zone_id) {
331 // Compare the zones for conflicts with comprehensive detection
332 if system_zone.id() != embedded_zone.id() {
333 conflicts.push(TimezoneConflict::IdMismatch {
334 zone_id: zone_id.to_string(),
335 system_id: system_zone.id().to_string(),
336 embedded_id: embedded_zone.id().to_string(),
337 });
338 }
339
340 // Check for offset conflicts at different times of year
341 let test_times = self.generate_test_timestamps();
342 for &timestamp in &test_times {
343 if let (Ok(system_offset), Ok(embedded_offset)) = (
344 system_zone.offset_millis_at_time(timestamp),
345 embedded_zone.offset_millis_at_time(timestamp)
346 ) {
347 if system_offset != embedded_offset {
348 conflicts.push(TimezoneConflict::OffsetMismatch {
349 zone_id: zone_id.to_string(),
350 timestamp,
351 system_offset,
352 embedded_offset,
353 });
354 }
355 }
356 }
357
358 // Check for DST transition differences
359 if let (Ok(system_dst), Ok(embedded_dst)) = (
360 self.get_dst_transitions(&system_zone),
361 self.get_dst_transitions(&embedded_zone)
362 ) {
363 if system_dst != embedded_dst {
364 conflicts.push(TimezoneConflict::DstMismatch {
365 zone_id: zone_id.to_string(),
366 system_transitions: system_dst,
367 embedded_transitions: embedded_dst,
368 });
369 }
370 }
371 }
372
373 Ok(conflicts)
374 }
375
376 pub fn clear_cache(&self) {
377 let mut cache = match self.cache.lock() {
378 Ok(guard) => guard,
379 Err(_) => {
380 eprintln!("Warning: Cache mutex poisoned in clear_cache");
381 return;
382 }
383 };
384 cache.data.clear();
385 cache.last_updated = SystemTime::UNIX_EPOCH;
386 }
387
388 pub fn cache_stats(&self) -> TimezoneStats {
389 let cache = match self.cache.lock() {
390 Ok(guard) => guard,
391 Err(_) => {
392 eprintln!("Warning: Cache mutex poisoned in cache_stats");
393 return TimezoneStats {
394 cached_zones: 0,
395 last_updated: SystemTime::UNIX_EPOCH,
396 cache_hit_rate: 0.0,
397 };
398 }
399 };
400 let total_requests = cache.hit_count + cache.miss_count;
401 let hit_rate = if total_requests > 0 {
402 cache.hit_count as f64 / total_requests as f64
403 } else {
404 0.0
405 };
406
407 TimezoneStats {
408 cached_zones: cache.data.len(),
409 last_updated: cache.last_updated,
410 cache_hit_rate: hit_rate,
411 }
412 }
413
414 pub fn list_system_timezones(&self) -> Outcome<Vec<String>> {
415 if !self.config.use_system_data || (self.config.require_consent && !self.has_consent()) {
416 return Ok(Vec::new());
417 }
418
419 let mut zone_ids = Vec::new();
420
421 for search_path in &self.config.search_paths {
422 if let Ok(zones) = self.scan_timezone_directory(search_path) {
423 zone_ids.extend(zones);
424 }
425 }
426
427 zone_ids.sort();
428 zone_ids.dedup();
429 Ok(zone_ids)
430 }
431
432 fn scan_timezone_directory(&self, dir: &Path) -> Outcome<Vec<String>> {
433 let mut zones = Vec::new();
434
435 if !dir.exists() {
436 return Ok(zones);
437 }
438
439 res!(self.scan_directory_recursive(dir, dir, &mut zones));
440 Ok(zones)
441 }
442
443 fn scan_directory_recursive(&self, base_dir: &Path, current_dir: &Path, zones: &mut Vec<String>) -> Outcome<()> {
444 for entry in res!(fs::read_dir(current_dir)) {
445 let entry = res!(entry);
446 let path = entry.path();
447
448 if path.is_dir() {
449 // Skip some known non-timezone directories
450 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
451 if matches!(name, "posix" | "right" | "." | "..") {
452 continue;
453 }
454 }
455
456 res!(self.scan_directory_recursive(base_dir, &path, zones));
457 } else if path.is_file() {
458 // Convert absolute path to relative zone ID
459 if let Ok(relative) = path.strip_prefix(base_dir) {
460 if let Some(zone_id) = relative.to_str() {
461 // Filter out obvious non-timezone files
462 if !zone_id.contains('.') && !zone_id.starts_with("leap") {
463 zones.push(zone_id.to_string());
464 }
465 }
466 }
467 }
468 }
469
470 Ok(())
471 }
472
473 fn generate_test_timestamps(&self) -> Vec<i64> {
474 let current_year = 2024; // Could be made dynamic
475 let mut timestamps = Vec::new();
476
477 // Test timestamps at different points throughout the year
478 // to catch seasonal DST differences
479 for month in [1, 3, 6, 9, 12] {
480 for day in [1, 15] {
481 for hour in [0, 12] {
482 // Convert to Unix timestamp (approximate)
483 let days_since_epoch = (current_year - 1970) * 365 + (current_year - 1969) / 4;
484 let month_days = (month - 1) * 30; // Simplified
485 let total_days = days_since_epoch + month_days + day - 1;
486 let timestamp = total_days as i64 * 86400 + hour * 3600;
487 timestamps.push(timestamp * 1000); // Convert to milliseconds
488 }
489 }
490 }
491
492 timestamps
493 }
494
495 fn get_dst_transitions(&self, _zone: &CalClockZone) -> Outcome<Vec<i64>> {
496 // This is a simplified implementation that would need to be
497 // expanded based on the actual timezone implementation
498 let transitions = Vec::new();
499
500 // For zones with DST rules, extract transition points
501 // This would require access to the zone's internal DST rules
502 // For now, return empty vector as placeholder
503
504 Ok(transitions)
505 }
506}
507
508#[derive(Clone, Debug)]
509pub enum TimezoneConflict {
510 IdMismatch {
511 zone_id: String,
512 system_id: String,
513 embedded_id: String,
514 },
515 OffsetMismatch {
516 zone_id: String,
517 timestamp: i64,
518 system_offset: i32,
519 embedded_offset: i32,
520 },
521 DstMismatch {
522 zone_id: String,
523 system_transitions: Vec<i64>,
524 embedded_transitions: Vec<i64>,
525 },
526}
527
528#[derive(Clone, Debug)]
529pub struct TimezoneStats {
530 pub cached_zones: usize,
531 pub last_updated: SystemTime,
532 pub cache_hit_rate: f64,
533}
534
535pub trait SystemTimezoneExt {
536 /// Attempts to load this timezone from system data, falling back to embedded data.
537 fn from_system_or_embedded(zone_id: &str) -> Outcome<CalClockZone>;
538
539 fn detect_conflicts(&self) -> Outcome<Vec<TimezoneConflict>>;
540
541 fn is_from_system(&self) -> bool;
542}
543
544impl SystemTimezoneExt for CalClockZone {
545 fn from_system_or_embedded(zone_id: &str) -> Outcome<CalClockZone> {
546 let manager = SystemTimezoneManager::global();
547
548 // Try system data first
549 if let Ok(Some(system_zone)) = manager.load_system_timezone(zone_id) {
550 return Ok(system_zone);
551 }
552
553 // Fall back to embedded data
554 CalClockZone::new(zone_id)
555 }
556
557 fn detect_conflicts(&self) -> Outcome<Vec<TimezoneConflict>> {
558 let manager = SystemTimezoneManager::global();
559 manager.detect_timezone_conflicts(self.id(), self)
560 }
561
562 fn is_from_system(&self) -> bool {
563 // TODO: Track this in CalClockZone
564 false
565 }
566}
567
568/// Timezone databases, IANA included, do not carry leap seconds: they work in
569/// UTC, which does not account for them. Leap seconds belong to the TAI-UTC
570/// layer instead.
571pub struct LeapSecondCapability;
572
573impl LeapSecondCapability {
574 pub fn handles_leap_seconds() -> bool {
575 false
576 }
577
578 pub fn leap_second_explanation() -> &'static str {
579 "Timezone databases (including IANA and Jiff-style integration) do NOT handle leap seconds. \
580 Leap seconds are handled separately through TAI-UTC conversion tables. Timezone data \
581 handles DST transitions and timezone rule changes, but leap seconds require a separate \
582 leap second table that tracks UTC-TAI differences over time."
583 }
584
585 pub fn requires_separate_implementation() -> bool {
586 true
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[test]
595 fn test_system_timezone_config_default() {
596 let config = SystemTimezoneConfig::default();
597 assert!(!config.use_system_data); // Conservative default
598 assert!(config.require_consent);
599 assert!(config.detect_conflicts);
600 assert!(!config.search_paths.is_empty());
601 }
602
603 #[test]
604 fn test_system_timezone_config_automatic() {
605 let config = SystemTimezoneConfig::automatic();
606 assert!(config.use_system_data);
607 assert!(!config.require_consent);
608 assert!(config.detect_conflicts);
609 }
610
611 #[test]
612 fn test_default_search_paths() {
613 let paths = SystemTimezoneConfig::default_search_paths();
614 assert!(!paths.is_empty());
615
616 if cfg!(unix) {
617 assert!(paths.contains(&PathBuf::from("/usr/share/zoneinfo")));
618 }
619 }
620
621 #[test]
622 fn test_leap_second_capability() {
623 assert!(!LeapSecondCapability::handles_leap_seconds());
624 assert!(LeapSecondCapability::requires_separate_implementation());
625 assert!(!LeapSecondCapability::leap_second_explanation().is_empty());
626 }
627
628 #[test]
629 fn test_timezone_manager_creation() {
630 let config = SystemTimezoneConfig::default();
631 let manager = SystemTimezoneManager::new(config);
632
633 // Initially no consent
634 assert!(!manager.has_consent());
635
636 // Stats should be empty
637 let stats = manager.cache_stats();
638 assert_eq!(stats.cached_zones, 0);
639 }
640
641 #[test]
642 fn test_timezone_manager_consent() {
643 let config = SystemTimezoneConfig::with_consent();
644 let manager = SystemTimezoneManager::new(config);
645
646 assert!(!manager.has_consent());
647
648 // Set environment variable to simulate consent
649 env::set_var("FE2O3_CALCLOCK_TIMEZONE_CONSENT", "1");
650 assert!(manager.request_consent().unwrap_or(false));
651 assert!(manager.has_consent());
652
653 // Clean up
654 env::remove_var("FE2O3_CALCLOCK_TIMEZONE_CONSENT");
655 }
656}