browser_forensic_core/
timestamp.rs1pub use forensicnomicon::heuristics::{CORE_DATA_EPOCH_OFFSET_SECS, WEBKIT_EPOCH_OFFSET_US};
4pub use forensicnomicon::temporal::FILETIME_EPOCH_OFFSET;
5
6pub fn webkit_micros_to_unix_nanos(webkit_us: i64) -> i64 {
11 webkit_us
12 .saturating_sub(WEBKIT_EPOCH_OFFSET_US)
13 .saturating_mul(1_000)
14}
15
16pub fn core_data_secs_to_unix_nanos(core_data_secs: f64) -> i64 {
17 ((core_data_secs as i64).saturating_add(CORE_DATA_EPOCH_OFFSET_SECS))
18 .saturating_mul(1_000_000_000)
19}
20
21pub fn unix_micros_to_nanos(us: i64) -> i64 {
22 us.saturating_mul(1_000)
23}
24
25pub fn unix_millis_to_nanos(ms: i64) -> i64 {
26 ms.saturating_mul(1_000_000)
27}
28
29pub fn unix_secs_to_nanos(secs: i64) -> i64 {
30 secs.saturating_mul(1_000_000_000)
31}
32
33pub fn unix_secs_f64_to_nanos(secs: f64) -> i64 {
37 let whole = secs.trunc() as i64;
38 let frac_ns = (secs.fract() * 1_000_000_000.0) as i64;
39 whole * 1_000_000_000 + frac_ns
40}
41
42#[must_use]
53pub fn filetime_to_unix_nanos(ft: u64) -> i64 {
54 let ticks_since_unix = i128::from(ft) - i128::from(FILETIME_EPOCH_OFFSET);
55 let nanos = ticks_since_unix * 100;
56 i64::try_from(nanos).unwrap_or(if nanos.is_negative() {
57 i64::MIN
58 } else {
59 i64::MAX
60 })
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn webkit_epoch_at_unix_epoch_returns_zero() {
69 let webkit_us = WEBKIT_EPOCH_OFFSET_US;
71 assert_eq!(webkit_micros_to_unix_nanos(webkit_us), 0);
72 }
73
74 #[test]
75 fn webkit_one_second_after_unix_epoch() {
76 let webkit_us = WEBKIT_EPOCH_OFFSET_US + 1_000_000;
77 assert_eq!(webkit_micros_to_unix_nanos(webkit_us), 1_000_000_000);
78 }
79
80 #[test]
81 fn core_data_epoch_at_unix_offset_returns_negative_secs() {
82 assert_eq!(
84 core_data_secs_to_unix_nanos(0.0),
85 CORE_DATA_EPOCH_OFFSET_SECS * 1_000_000_000
86 );
87 }
88
89 #[test]
90 fn core_data_one_second_later() {
91 assert_eq!(
92 core_data_secs_to_unix_nanos(1.0),
93 (CORE_DATA_EPOCH_OFFSET_SECS + 1) * 1_000_000_000
94 );
95 }
96
97 #[test]
98 fn unix_micros_to_nanos_multiplies_by_1000() {
99 assert_eq!(unix_micros_to_nanos(1_000_000), 1_000_000_000);
100 assert_eq!(unix_micros_to_nanos(0), 0);
101 }
102
103 #[test]
104 fn unix_millis_to_nanos_multiplies_by_1_000_000() {
105 assert_eq!(unix_millis_to_nanos(1_000), 1_000_000_000);
106 }
107
108 #[test]
109 fn unix_secs_to_nanos_multiplies_by_1_000_000_000() {
110 assert_eq!(unix_secs_to_nanos(1), 1_000_000_000);
111 }
112
113 #[test]
114 fn conversions_saturate_on_adversarial_input_never_panic() {
115 assert_eq!(webkit_micros_to_unix_nanos(i64::MAX), i64::MAX);
118 assert_eq!(webkit_micros_to_unix_nanos(i64::MIN), i64::MIN);
119 assert_eq!(unix_secs_to_nanos(i64::MAX), i64::MAX);
120 assert_eq!(unix_millis_to_nanos(i64::MIN), i64::MIN);
121 assert_eq!(unix_micros_to_nanos(i64::MAX), i64::MAX);
122 }
123
124 #[test]
125 fn unix_secs_f64_preserves_subsecond() {
126 assert_eq!(unix_secs_f64_to_nanos(1.0), 1_000_000_000);
127 assert_eq!(
128 unix_secs_f64_to_nanos(1_700_000_000.5),
129 1_700_000_000_500_000_000
130 );
131 assert_eq!(unix_secs_f64_to_nanos(0.0), 0);
132 }
133
134 #[test]
135 fn filetime_at_unix_epoch_returns_zero() {
136 assert_eq!(filetime_to_unix_nanos(FILETIME_EPOCH_OFFSET), 0);
138 }
139
140 #[test]
141 fn filetime_one_second_after_unix_epoch() {
142 let ft = FILETIME_EPOCH_OFFSET + 10_000_000;
144 assert_eq!(filetime_to_unix_nanos(ft), 1_000_000_000);
145 }
146
147 #[test]
148 fn filetime_preserves_100ns_precision() {
149 let ft = FILETIME_EPOCH_OFFSET + 1;
151 assert_eq!(filetime_to_unix_nanos(ft), 100);
152 }
153
154 #[test]
155 fn filetime_known_value_2023_01_01() {
156 let ft = (1_672_531_200u64 + 11_644_473_600u64) * 10_000_000;
159 assert_eq!(filetime_to_unix_nanos(ft), 1_672_531_200_000_000_000);
160 }
161
162 #[test]
163 fn filetime_adversarial_extremes_never_panic() {
164 assert_eq!(filetime_to_unix_nanos(u64::MAX), i64::MAX);
167 assert_eq!(filetime_to_unix_nanos(0), i64::MIN);
169 }
170}