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 chrono::{DateTime, Utc};
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 [`DateTime`].
52///
53/// FILETIME counts 100-nanosecond ticks since 1601-01-01. Values before the
54/// Unix epoch are clamped to `DateTime::UNIX_EPOCH`.
55pub fn filetime_to_datetime(filetime: u64) -> DateTime<Utc> {
56    let unix_100ns = filetime.saturating_sub(FILETIME_EPOCH_OFFSET);
57    let secs = i64::try_from(unix_100ns / 10_000_000).unwrap_or(i64::MAX);
58    let nanos = u32::try_from((unix_100ns % 10_000_000) * 100).unwrap_or(0);
59    DateTime::from_timestamp(secs, nanos).unwrap_or(DateTime::UNIX_EPOCH.with_timezone(&Utc))
60}
61
62/// Convert an OLE Automation Date (f64) to a UTC [`DateTime`].
63///
64/// OLE date counts days since 1899-12-30. The Unix epoch is 25569 days after
65/// the OLE epoch. Infinite or NaN values are clamped to `DateTime::UNIX_EPOCH`.
66pub fn ole_date_to_datetime(v: f64) -> DateTime<Utc> {
67    const OLE_TO_UNIX_DAYS: f64 = 25569.0;
68    if !v.is_finite() {
69        return DateTime::UNIX_EPOCH.with_timezone(&Utc);
70    }
71    let unix_secs_f64 = (v - OLE_TO_UNIX_DAYS) * 86400.0;
72    let unix_secs = unix_secs_f64 as i64;
73    let nanos = ((unix_secs_f64 - unix_secs as f64).abs() * 1_000_000_000.0) as u32;
74    DateTime::from_timestamp(unix_secs, nanos).unwrap_or(DateTime::UNIX_EPOCH.with_timezone(&Utc))
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn filetime_to_datetime_unix_epoch() {
83        let dt = filetime_to_datetime(FILETIME_EPOCH_OFFSET);
84        assert_eq!(dt.timestamp(), 0, "must map to Unix epoch");
85    }
86
87    #[test]
88    fn filetime_to_datetime_known_date() {
89        // 2024-06-15T08:00:00Z = Unix 1718438400
90        let filetime = FILETIME_EPOCH_OFFSET + 1_718_438_400u64 * 10_000_000;
91        let dt = filetime_to_datetime(filetime);
92        assert_eq!(dt.timestamp(), 1_718_438_400);
93    }
94
95    #[test]
96    fn record_size_constants_are_32() {
97        assert_eq!(NETWORK_RECORD_SIZE, 32usize);
98        assert_eq!(APP_RECORD_SIZE, 32usize);
99    }
100
101    #[test]
102    fn network_record_has_bytes_sent() {
103        let r = NetworkUsageRecord {
104            bytes_sent: 1024,
105            bytes_recv: 0,
106            timestamp: chrono::DateTime::UNIX_EPOCH.with_timezone(&chrono::Utc),
107            app_id: 1,
108            user_id: 0,
109            auto_inc_id: 0,
110        };
111        assert_eq!(r.bytes_sent, 1024);
112    }
113
114    #[test]
115    fn network_record_has_bytes_recv() {
116        let r = NetworkUsageRecord {
117            bytes_sent: 0,
118            bytes_recv: 2048,
119            timestamp: chrono::DateTime::UNIX_EPOCH.with_timezone(&chrono::Utc),
120            app_id: 1,
121            user_id: 0,
122            auto_inc_id: 0,
123        };
124        assert_eq!(r.bytes_recv, 2048);
125    }
126
127    #[test]
128    fn network_record_has_timestamp() {
129        let ts = chrono::DateTime::UNIX_EPOCH.with_timezone(&chrono::Utc);
130        let r = NetworkUsageRecord {
131            bytes_sent: 0,
132            bytes_recv: 0,
133            timestamp: ts,
134            app_id: 1,
135            user_id: 0,
136            auto_inc_id: 0,
137        };
138        let _ = r.timestamp;
139    }
140
141    #[test]
142    fn network_record_has_app_id() {
143        let r = NetworkUsageRecord {
144            bytes_sent: 0,
145            bytes_recv: 0,
146            timestamp: chrono::DateTime::UNIX_EPOCH.with_timezone(&chrono::Utc),
147            app_id: 42,
148            user_id: 0,
149            auto_inc_id: 0,
150        };
151        assert_eq!(r.app_id, 42_i32);
152    }
153
154    #[test]
155    fn app_usage_record_has_foreground_cycles() {
156        let r = AppUsageRecord {
157            app_id: 1,
158            user_id: 0,
159            timestamp: chrono::DateTime::UNIX_EPOCH.with_timezone(&chrono::Utc),
160            foreground_cycles: 999_000,
161            background_cycles: 0,
162            auto_inc_id: 0,
163        };
164        assert_eq!(r.foreground_cycles, 999_000_u64);
165    }
166
167    #[test]
168    fn id_map_entry_has_id_and_name() {
169        let e = IdMapEntry {
170            id: 7,
171            name: "explorer.exe".to_owned(),
172        };
173        assert_eq!(e.id, 7_i32);
174        assert_eq!(e.name, "explorer.exe");
175    }
176
177    #[test]
178    fn network_record_serializes_to_json() {
179        let r = NetworkUsageRecord {
180            bytes_sent: 512,
181            bytes_recv: 1024,
182            timestamp: chrono::DateTime::UNIX_EPOCH.with_timezone(&chrono::Utc),
183            app_id: 3,
184            user_id: 1,
185            auto_inc_id: 0,
186        };
187        let json = serde_json::to_string(&r).expect("serialise to JSON");
188        assert!(json.contains("bytes_sent"));
189        assert!(json.contains("512"));
190        // auto_inc_id must NOT appear in JSON output (#[serde(skip)])
191        assert!(!json.contains("auto_inc_id"));
192    }
193}