git_policy_cli/
claim_time.rs1use super::service_replay::capture_stamp;
19use std::sync::OnceLock;
20use std::time::{Duration, SystemTime, UNIX_EPOCH};
21
22const FIELD_SEPARATOR: char = '~';
24
25const BOOT_ID_LENGTH: usize = 32;
27
28#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ClaimTime {
39 boot_id: String,
41 monotonic_millis: u64,
43 wall_seconds: u64,
45}
46
47fn current_boot_id() -> &'static str {
49 static BOOT_ID: OnceLock<String> = OnceLock::new();
50 return BOOT_ID
51 .get_or_init(|| {
52 let Ok(text) = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") else {
53 return String::new();
54 };
55 let compact: String = text.trim().chars().filter(|character| { return *character != '-'; }).collect();
56 return if compact.len() == BOOT_ID_LENGTH && compact.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }) { compact } else { String::new() };
57 })
58 .as_str();
59}
60
61impl ClaimTime {
63 pub fn read() -> ClaimTime {
65 let monotonic_millis: u64 = capture_stamp() / 1_000_000;
66 let boot_id: String = if monotonic_millis == 0 { String::new() } else { String::from(current_boot_id()) };
68 let wall_seconds: u64 = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |elapsed| { return elapsed.as_secs(); });
69 return ClaimTime { boot_id, monotonic_millis, wall_seconds };
70 }
71
72 pub fn from_parts(boot_id: &str, monotonic: Duration, wall: Duration) -> ClaimTime {
74 return ClaimTime {
75 boot_id: boot_id.to_owned(),
76 monotonic_millis: u64::try_from(monotonic.as_millis()).unwrap_or(u64::MAX),
77 wall_seconds: wall.as_secs(),
78 };
79 }
80
81 pub fn advanced(&self, by: Duration) -> ClaimTime {
83 return ClaimTime {
84 boot_id: self.boot_id.clone(),
85 monotonic_millis: self.monotonic_millis.saturating_add(u64::try_from(by.as_millis()).unwrap_or(u64::MAX)),
86 wall_seconds: self.wall_seconds.saturating_add(by.as_secs()),
87 };
88 }
89
90 pub fn rewound(&self, by: Duration) -> ClaimTime {
92 return ClaimTime {
93 boot_id: self.boot_id.clone(),
94 monotonic_millis: self.monotonic_millis.saturating_sub(u64::try_from(by.as_millis()).unwrap_or(u64::MAX)),
95 wall_seconds: self.wall_seconds.saturating_sub(by.as_secs()),
96 };
97 }
98
99 pub fn wall_stepped_forward(&self, by: Duration) -> ClaimTime {
101 return ClaimTime { wall_seconds: self.wall_seconds.saturating_add(by.as_secs()), ..self.clone() };
102 }
103
104 pub fn wall_stepped_back(&self, by: Duration) -> ClaimTime {
106 return ClaimTime { wall_seconds: self.wall_seconds.saturating_sub(by.as_secs()), ..self.clone() };
107 }
108
109 pub fn in_boot(&self, boot_id: &str) -> ClaimTime {
111 return ClaimTime { boot_id: boot_id.to_owned(), ..self.clone() };
112 }
113
114 pub fn from_wall(wall: SystemTime) -> ClaimTime {
117 return ClaimTime {
118 boot_id: String::new(),
119 monotonic_millis: 0,
120 wall_seconds: wall.duration_since(UNIX_EPOCH).map_or(0, |elapsed| { return elapsed.as_secs(); }),
121 };
122 }
123
124 pub fn elapsed_since(&self, earlier: &ClaimTime) -> Duration {
127 if !self.boot_id.is_empty() && self.boot_id == earlier.boot_id {
128 return Duration::from_millis(self.monotonic_millis.saturating_sub(earlier.monotonic_millis));
129 }
130 return Duration::from_secs(self.wall_seconds.saturating_sub(earlier.wall_seconds));
131 }
132
133 pub fn encode(&self) -> String {
135 return format!("{}{FIELD_SEPARATOR}{}{FIELD_SEPARATOR}{}", self.boot_id, self.monotonic_millis, self.wall_seconds);
136 }
137
138 pub fn decode(text: &str) -> Option<ClaimTime> {
140 let mut parts = text.split(FIELD_SEPARATOR);
141 let boot_id: &str = parts.next()?;
142 let monotonic_millis: u64 = parts.next()?.parse().ok()?;
143 let wall_seconds: u64 = parts.next()?.parse().ok()?;
144 if parts.next().is_some() || !(boot_id.is_empty() || (boot_id.len() == BOOT_ID_LENGTH && boot_id.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }))) {
145 return None;
146 }
147 return Some(ClaimTime { boot_id: boot_id.to_owned(), monotonic_millis, wall_seconds });
148 }
149}
150
151#[cfg(test)]
153#[path = "claim_time_tests.rs"]
154mod tests;