Skip to main content

fuchsia_rtc/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fidl_next_fuchsia_hardware_rtc as frtc;
6use log::warn;
7
8// Time conversion constants (same as pl031-rtc)
9pub const LOCAL_EPOCH: u64 = 946684800;
10pub const LOCAL_EPOCH_YEAR: u16 = 2000;
11pub const DEFAULT_YEAR: u16 = 2020;
12pub const MAX_YEAR: u16 = 2099;
13
14pub const DAYS_IN_MONTH: [u64; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
15
16pub const DEFAULT_RTC: frtc::Time = frtc::Time {
17    seconds: 0,
18    minutes: 0,
19    hours: 0,
20    day: 1,
21    month: 1, // JANUARY
22    year: DEFAULT_YEAR,
23};
24
25pub fn is_leap_year(year: u16) -> bool {
26    ((year % 4) == 0 && (year % 100) != 0) || ((year % 400) == 0)
27}
28
29pub fn days_in_year(year: u16) -> u64 {
30    if is_leap_year(year) { 366 } else { 365 }
31}
32
33pub fn days_in_month(month: u8, year: u16) -> u64 {
34    let mut days = DAYS_IN_MONTH[month as usize];
35    if month == 2 && is_leap_year(year) {
36        days += 1;
37    }
38    days
39}
40
41pub fn is_rtc_valid(rtc: &frtc::Time) -> bool {
42    if rtc.year < LOCAL_EPOCH_YEAR || rtc.year > MAX_YEAR {
43        return false;
44    }
45    if rtc.month < 1 || rtc.month > 12 {
46        return false;
47    }
48    if rtc.day as u64 > days_in_month(rtc.month, rtc.year) {
49        return false;
50    }
51    if rtc.day == 0 {
52        return false;
53    }
54    if rtc.hours > 23 || rtc.minutes > 59 || rtc.seconds > 59 {
55        return false;
56    }
57    true
58}
59
60pub fn seconds_to_rtc(seconds: u64) -> frtc::Time {
61    if seconds < LOCAL_EPOCH {
62        warn!("SecondsToRtc: Seconds value is out of range, returning default");
63        return DEFAULT_RTC;
64    }
65
66    let mut epoch = seconds - LOCAL_EPOCH;
67    let mut rtc = frtc::Time {
68        seconds: (epoch % 60) as u8,
69        minutes: 0,
70        hours: 0,
71        day: 0,
72        month: 0,
73        year: LOCAL_EPOCH_YEAR,
74    };
75    epoch /= 60;
76    rtc.minutes = (epoch % 60) as u8;
77    epoch /= 60;
78    rtc.hours = (epoch % 24) as u8;
79    epoch /= 24;
80
81    for year in LOCAL_EPOCH_YEAR.. {
82        let days = days_in_year(year);
83        if epoch < days {
84            rtc.year = year;
85            break;
86        }
87        epoch -= days;
88    }
89
90    for month in 1..=12 {
91        let days = days_in_month(month, rtc.year);
92        if epoch < days {
93            rtc.month = month;
94            break;
95        }
96        epoch -= days;
97    }
98
99    rtc.day = (epoch + 1) as u8;
100    rtc
101}
102
103pub fn seconds_since_epoch(rtc: &frtc::Time) -> u64 {
104    let mut days_since_local_epoch = 0;
105    for year in LOCAL_EPOCH_YEAR..rtc.year {
106        days_since_local_epoch += days_in_year(year);
107    }
108    for month in 1..rtc.month {
109        days_since_local_epoch += days_in_month(month, rtc.year);
110    }
111    days_since_local_epoch += (rtc.day - 1) as u64;
112
113    let hours = (days_since_local_epoch * 24) + rtc.hours as u64;
114    let minutes = (hours * 60) + rtc.minutes as u64;
115    let seconds = (minutes * 60) + rtc.seconds as u64;
116
117    LOCAL_EPOCH + seconds
118}
119
120pub fn sanitize_rtc(rtc: frtc::Time) -> frtc::Time {
121    if !is_rtc_valid(&rtc) || rtc.year < DEFAULT_YEAR {
122        warn!("RTC is sanitized to constant default");
123        return DEFAULT_RTC;
124    }
125    rtc
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_is_leap_year() {
134        assert!(is_leap_year(2000));
135        assert!(is_leap_year(2020));
136        assert!(is_leap_year(2024));
137        assert!(!is_leap_year(2021));
138        assert!(!is_leap_year(2022));
139        assert!(!is_leap_year(2023));
140        assert!(!is_leap_year(2100));
141    }
142
143    #[test]
144    fn test_is_rtc_valid() {
145        let valid =
146            frtc::Time { seconds: 30, minutes: 15, hours: 10, day: 15, month: 6, year: 2024 };
147        assert!(is_rtc_valid(&valid));
148
149        let invalid_year = frtc::Time { year: 1999, ..valid };
150        assert!(!is_rtc_valid(&invalid_year));
151
152        let invalid_year_high = frtc::Time { year: 2100, ..valid };
153        assert!(!is_rtc_valid(&invalid_year_high));
154
155        let invalid_month = frtc::Time { month: 13, ..valid };
156        assert!(!is_rtc_valid(&invalid_month));
157
158        let invalid_day_zero = frtc::Time { day: 0, ..valid };
159        assert!(!is_rtc_valid(&invalid_day_zero));
160
161        let invalid_feb = frtc::Time { day: 29, month: 2, year: 2023, ..valid };
162        assert!(!is_rtc_valid(&invalid_feb));
163
164        let valid_feb = frtc::Time { day: 29, month: 2, year: 2024, ..valid };
165        assert!(is_rtc_valid(&valid_feb));
166
167        let invalid_hours = frtc::Time { hours: 24, ..valid };
168        assert!(!is_rtc_valid(&invalid_hours));
169
170        let invalid_minutes = frtc::Time { minutes: 60, ..valid };
171        assert!(!is_rtc_valid(&invalid_minutes));
172
173        let invalid_seconds = frtc::Time { seconds: 60, ..valid };
174        assert!(!is_rtc_valid(&invalid_seconds));
175    }
176
177    #[test]
178    fn test_seconds_conversion_roundtrip() {
179        let time =
180            frtc::Time { seconds: 45, minutes: 30, hours: 14, day: 28, month: 2, year: 2024 };
181        let seconds = seconds_since_epoch(&time);
182        let converted = seconds_to_rtc(seconds);
183        assert_eq!(converted.seconds, time.seconds);
184        assert_eq!(converted.minutes, time.minutes);
185        assert_eq!(converted.hours, time.hours);
186        assert_eq!(converted.day, time.day);
187        assert_eq!(converted.month, time.month);
188        assert_eq!(converted.year, time.year);
189    }
190
191    #[test]
192    fn test_sanitize_rtc() {
193        let invalid = frtc::Time { seconds: 0, minutes: 0, hours: 0, day: 1, month: 1, year: 2010 };
194        let sanitized = sanitize_rtc(invalid);
195        assert_eq!(sanitized.year, DEFAULT_YEAR);
196        assert_eq!(sanitized.month, 1);
197        assert_eq!(sanitized.day, 1);
198
199        let valid =
200            frtc::Time { seconds: 10, minutes: 20, hours: 8, day: 14, month: 10, year: 2026 };
201        let kept = sanitize_rtc(valid);
202        assert_eq!(kept.year, 2026);
203        assert_eq!(kept.month, 10);
204        assert_eq!(kept.day, 14);
205    }
206}