Skip to main content

srum_core/
lib.rs

1//! SRUM (System Resource Usage Monitor) record type definitions.
2//!
3//! These are pure data types with no parsing logic.
4//! Parsing is handled by the `srum-parser` crate.
5
6pub mod app_timeline;
7pub mod app_usage;
8pub mod connectivity;
9pub mod energy;
10pub mod energy_lt;
11pub mod id_map;
12pub mod network;
13pub mod push_notification;
14
15pub use app_timeline::AppTimelineRecord;
16pub use app_usage::AppUsageRecord;
17pub use connectivity::NetworkConnectivityRecord;
18pub use energy::EnergyUsageRecord;
19pub use energy_lt::EnergyLtRecord;
20pub use id_map::IdMapEntry;
21pub use network::NetworkUsageRecord;
22pub use push_notification::PushNotificationRecord;
23
24use jiff::Timestamp;
25
26/// Number of 100ns ticks between the Windows epoch (1601-01-01) and the
27/// Unix epoch (1970-01-01).
28pub const FILETIME_EPOCH_OFFSET: u64 = 116_444_736_000_000_000;
29
30/// Fixed byte length of a serialised [`NetworkUsageRecord`].
31pub const NETWORK_RECORD_SIZE: usize = 32;
32
33/// Fixed byte length of a serialised [`AppUsageRecord`].
34pub const APP_RECORD_SIZE: usize = 32;
35
36/// Fixed byte length of a serialised [`AppTimelineRecord`].
37pub const APP_TIMELINE_RECORD_SIZE: usize = 32;
38
39/// Fixed byte length of a serialised [`NetworkConnectivityRecord`].
40pub const NETWORK_CONNECTIVITY_RECORD_SIZE: usize = 28;
41
42/// Fixed byte length of a serialised [`EnergyUsageRecord`].
43pub const ENERGY_RECORD_SIZE: usize = 32;
44
45/// Minimum byte length of a serialised [`PushNotificationRecord`].
46pub const PUSH_NOTIFICATION_RECORD_SIZE: usize = 24;
47
48/// Minimum byte length of a serialised [`IdMapEntry`].
49pub const ID_MAP_MIN_SIZE: usize = 6;
50
51/// Convert a Windows FILETIME value to a UTC [`Timestamp`].
52///
53/// FILETIME counts 100-nanosecond ticks since 1601-01-01. Values before the
54/// Unix epoch are clamped to `Timestamp::UNIX_EPOCH`.
55pub fn filetime_to_datetime(filetime: u64) -> Timestamp {
56    let unix_100ns = filetime.saturating_sub(FILETIME_EPOCH_OFFSET);
57    let unix_nanos = i128::from(unix_100ns) * 100;
58    Timestamp::from_nanosecond(unix_nanos).unwrap_or(Timestamp::UNIX_EPOCH)
59}
60
61/// Convert an OLE Automation Date (f64) to a UTC [`Timestamp`].
62///
63/// OLE date counts days since 1899-12-30. The Unix epoch is 25569 days after
64/// the OLE epoch. Infinite or NaN values are clamped to `Timestamp::UNIX_EPOCH`.
65pub fn ole_date_to_datetime(v: f64) -> Timestamp {
66    const OLE_TO_UNIX_DAYS: f64 = 25569.0;
67    if !v.is_finite() {
68        return Timestamp::UNIX_EPOCH;
69    }
70    let unix_secs_f64 = (v - OLE_TO_UNIX_DAYS) * 86400.0;
71    let unix_secs = unix_secs_f64 as i64;
72    let nanos = ((unix_secs_f64 - unix_secs as f64).abs() * 1_000_000_000.0) as u32;
73    Timestamp::new(unix_secs, nanos as i32).unwrap_or(Timestamp::UNIX_EPOCH)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn filetime_to_datetime_unix_epoch() {
82        let dt = filetime_to_datetime(FILETIME_EPOCH_OFFSET);
83        assert_eq!(dt.as_second(), 0, "must map to Unix epoch");
84    }
85
86    #[test]
87    fn filetime_to_datetime_known_date() {
88        // 2024-06-15T08:00:00Z = Unix 1718438400
89        let filetime = FILETIME_EPOCH_OFFSET + 1_718_438_400u64 * 10_000_000;
90        let dt = filetime_to_datetime(filetime);
91        assert_eq!(dt.as_second(), 1_718_438_400);
92    }
93
94    #[test]
95    fn filetime_to_datetime_sub_second() {
96        // OFFSET + 12345 ticks (100ns each) = 1970-01-01T00:00:00.001234500Z.
97        // Ground truth derived from the documented FILETIME math:
98        // 12345 * 100 ns = 1_234_500 ns after the Unix epoch.
99        let dt = filetime_to_datetime(FILETIME_EPOCH_OFFSET + 12_345);
100        assert_eq!(dt.as_second(), 0);
101        assert_eq!(dt.as_nanosecond(), 1_234_500_i128);
102    }
103
104    #[test]
105    fn ole_date_to_datetime_unix_epoch() {
106        // OLE epoch is 1899-12-30; the Unix epoch is 25569 days later.
107        let dt = ole_date_to_datetime(25569.0);
108        assert_eq!(dt.as_second(), 0, "OLE day 25569 == Unix epoch");
109    }
110
111    #[test]
112    fn ole_date_to_datetime_known_date() {
113        // OLE day 45000 (a whole-day value that converts exactly, avoiding f64
114        // truncation artifacts) == 2023-03-15T00:00:00Z == Unix 1678838400.
115        // Ground truth derived from the documented OLE conversion:
116        // (45000 - 25569) * 86400 = 19431 * 86400 = 1678838400.
117        let dt = ole_date_to_datetime(45000.0);
118        assert_eq!(dt.as_second(), 1_678_838_400);
119    }
120
121    #[test]
122    fn ole_date_to_datetime_non_finite_clamps_to_epoch() {
123        assert_eq!(ole_date_to_datetime(f64::NAN).as_second(), 0);
124        assert_eq!(ole_date_to_datetime(f64::INFINITY).as_second(), 0);
125    }
126
127    #[test]
128    fn record_size_constants_are_32() {
129        assert_eq!(NETWORK_RECORD_SIZE, 32usize);
130        assert_eq!(APP_RECORD_SIZE, 32usize);
131    }
132
133    #[test]
134    fn network_record_has_bytes_sent() {
135        let r = NetworkUsageRecord {
136            bytes_sent: 1024,
137            bytes_recv: 0,
138            timestamp: Timestamp::UNIX_EPOCH,
139            app_id: 1,
140            user_id: 0,
141            auto_inc_id: 0,
142        };
143        assert_eq!(r.bytes_sent, 1024);
144    }
145
146    #[test]
147    fn network_record_has_bytes_recv() {
148        let r = NetworkUsageRecord {
149            bytes_sent: 0,
150            bytes_recv: 2048,
151            timestamp: Timestamp::UNIX_EPOCH,
152            app_id: 1,
153            user_id: 0,
154            auto_inc_id: 0,
155        };
156        assert_eq!(r.bytes_recv, 2048);
157    }
158
159    #[test]
160    fn network_record_has_timestamp() {
161        let ts = Timestamp::UNIX_EPOCH;
162        let r = NetworkUsageRecord {
163            bytes_sent: 0,
164            bytes_recv: 0,
165            timestamp: ts,
166            app_id: 1,
167            user_id: 0,
168            auto_inc_id: 0,
169        };
170        let _ = r.timestamp;
171    }
172
173    #[test]
174    fn network_record_has_app_id() {
175        let r = NetworkUsageRecord {
176            bytes_sent: 0,
177            bytes_recv: 0,
178            timestamp: Timestamp::UNIX_EPOCH,
179            app_id: 42,
180            user_id: 0,
181            auto_inc_id: 0,
182        };
183        assert_eq!(r.app_id, 42_i32);
184    }
185
186    #[test]
187    fn app_usage_record_has_foreground_cycles() {
188        let r = AppUsageRecord {
189            app_id: 1,
190            user_id: 0,
191            timestamp: Timestamp::UNIX_EPOCH,
192            foreground_cycles: 999_000,
193            background_cycles: 0,
194            auto_inc_id: 0,
195        };
196        assert_eq!(r.foreground_cycles, 999_000_u64);
197    }
198
199    #[test]
200    fn id_map_entry_has_id_and_name() {
201        let e = IdMapEntry {
202            id: 7,
203            name: "explorer.exe".to_owned(),
204        };
205        assert_eq!(e.id, 7_i32);
206        assert_eq!(e.name, "explorer.exe");
207    }
208
209    #[test]
210    fn network_record_serializes_to_json() {
211        let r = NetworkUsageRecord {
212            bytes_sent: 512,
213            bytes_recv: 1024,
214            timestamp: Timestamp::UNIX_EPOCH,
215            app_id: 3,
216            user_id: 1,
217            auto_inc_id: 0,
218        };
219        let json = serde_json::to_string(&r).expect("serialise to JSON");
220        assert!(json.contains("bytes_sent"));
221        assert!(json.contains("512"));
222        // auto_inc_id must NOT appear in JSON output (#[serde(skip)])
223        assert!(!json.contains("auto_inc_id"));
224    }
225}