tracexec_core/
timestamp.rs

1use std::{
2  borrow::Cow,
3  sync::LazyLock,
4};
5
6use chrono::{
7  DateTime,
8  Local,
9};
10use nutype::nutype;
11
12#[nutype(
13  validate(with = validate_strftime, error = Cow<'static,str>),
14  derive(Debug, Clone, Serialize, Deserialize, Deref, FromStr)
15)]
16pub struct TimestampFormat(String);
17
18fn validate_strftime(fmt: &str) -> Result<(), Cow<'static, str>> {
19  if fmt.contains("\n") {
20    return Err("inline timestamp format string should not contain newline(s)".into());
21  }
22  Ok(())
23}
24
25pub type Timestamp = DateTime<Local>;
26
27pub fn ts_from_boot_ns(boot_ns: u64) -> Timestamp {
28  DateTime::from_timestamp_nanos((*BOOT_TIME + boot_ns) as i64).into()
29}
30
31static BOOT_TIME: LazyLock<u64> = LazyLock::new(|| {
32  let content = std::fs::read_to_string("/proc/stat").expect("Failed to read /proc/stat");
33  for line in content.lines() {
34    if line.starts_with("btime") {
35      return line
36        .split(' ')
37        .nth(1)
38        .unwrap()
39        .parse::<u64>()
40        .expect("Failed to parse btime in /proc/stat")
41        * 1_000_000_000;
42    }
43  }
44  panic!("btime is not available in /proc/stat. Am I running on Linux?")
45});
46
47#[cfg(test)]
48mod tests {
49  use std::str::FromStr;
50
51  use chrono::{
52    DateTime,
53    Local,
54  };
55
56  use super::*;
57
58  /* ---------------- TimestampFormat ---------------- */
59
60  #[test]
61  fn timestamp_format_accepts_valid_strftime() {
62    let fmt = TimestampFormat::from_str("%Y-%m-%d %H:%M:%S");
63    assert!(fmt.is_ok());
64  }
65
66  #[test]
67  fn timestamp_format_rejects_newline() {
68    let fmt = TimestampFormat::from_str("%Y-%m-%d\n%H:%M:%S");
69    assert!(fmt.is_err());
70
71    let err = fmt.unwrap_err();
72    assert!(
73      err.contains("should not contain newline"),
74      "unexpected error message: {err}"
75    );
76  }
77
78  #[test]
79  fn timestamp_format_deref_works() {
80    let fmt = TimestampFormat::from_str("%s").unwrap();
81    assert_eq!(&*fmt, "%s");
82  }
83
84  /* ---------------- BOOT_TIME ---------------- */
85
86  #[test]
87  fn boot_time_is_non_zero() {
88    assert!(*BOOT_TIME > 0);
89  }
90
91  #[test]
92  fn boot_time_is_reasonable_unix_time() {
93    // boot time should be after year 2000
94    const YEAR_2000_NS: u64 = 946684800_u64 * 1_000_000_000;
95    assert!(
96      *BOOT_TIME > YEAR_2000_NS,
97      "BOOT_TIME too small: {}",
98      *BOOT_TIME
99    );
100  }
101
102  /* ---------------- ts_from_boot_ns ---------------- */
103
104  #[test]
105  fn ts_from_boot_ns_zero_matches_boot_time() {
106    let ts = ts_from_boot_ns(0);
107    let expected: DateTime<Local> = DateTime::from_timestamp_nanos(*BOOT_TIME as i64).into();
108
109    assert_eq!(ts, expected);
110  }
111
112  #[test]
113  fn ts_from_boot_ns_is_monotonic() {
114    let t1 = ts_from_boot_ns(1_000);
115    let t2 = ts_from_boot_ns(2_000);
116
117    assert!(t2 > t1);
118  }
119
120  #[test]
121  fn ts_from_boot_ns_large_offset() {
122    let one_sec = 1_000_000_000;
123    let ts = ts_from_boot_ns(one_sec);
124
125    let base: DateTime<Local> = DateTime::from_timestamp_nanos(*BOOT_TIME as i64).into();
126
127    assert_eq!(ts.timestamp(), base.timestamp() + 1);
128  }
129
130  /* ---------------- serde (nutype derive) ---------------- */
131
132  #[test]
133  fn timestamp_format_serde_roundtrip() {
134    let fmt = TimestampFormat::from_str("%H:%M:%S").unwrap();
135
136    let json = serde_json::to_string(&fmt).unwrap();
137    let de: TimestampFormat = serde_json::from_str(&json).unwrap();
138
139    assert_eq!(&*fmt, &*de);
140  }
141}