use crate::service_replay::capture_stamp;
use std::path::Path;
use std::sync::OnceLock;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const FIELD_SEPARATOR: char = '~';
const BOOT_ID_LENGTH: usize = 32;
const MACHINE_ID_PATHS: [&str; 2] = ["/etc/machine-id", "/var/lib/dbus/machine-id"];
const PARTS_WITHOUT_MACHINE: usize = 3;
const PARTS_WITH_MACHINE: usize = 4;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClaimTime {
machine_id: String,
boot_id: String,
monotonic_millis: u64,
wall_seconds: u64,
}
fn is_compact_hex_id(text: &str) -> bool {
return text.len() == BOOT_ID_LENGTH && text.as_bytes().iter().all(u8::is_ascii_hexdigit);
}
fn is_lowercase_hex_digit(byte: &u8) -> bool {
return byte.is_ascii_digit() || (b'a'..=b'f').contains(byte);
}
fn read_boot_id() -> String {
let Ok(text) = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") else {
return String::new();
};
let compact: String = text.trim().replace('-', "");
return if is_compact_hex_id(compact.as_str()) { compact } else { String::new() };
}
fn read_machine_id() -> String {
let mut paths: Vec<&Path> = Vec::new();
for path in MACHINE_ID_PATHS {
paths.push(Path::new(path));
}
return machine_id_from(paths.as_slice());
}
fn seconds_since_epoch(time: SystemTime) -> u64 {
return match time.duration_since(UNIX_EPOCH) {
Ok(elapsed) => elapsed.as_secs(),
Err(_before_the_epoch) => 0,
};
}
pub(crate) fn is_machine_id(text: &str) -> bool {
return text.len() == BOOT_ID_LENGTH
&& text.as_bytes().iter().all(is_lowercase_hex_digit)
&& !text.trim_start_matches('0').is_empty();
}
pub(crate) fn current_boot_id() -> &'static str {
static BOOT_ID: OnceLock<String> = OnceLock::new();
return BOOT_ID
.get_or_init(read_boot_id)
.as_str();
}
fn machine_id_from(paths: &[&Path]) -> String {
for path in paths {
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if is_machine_id(text.trim()) {
return text.trim().to_owned();
}
}
return String::new();
}
pub(crate) fn current_machine_id() -> &'static str {
#[cfg(test)]
if let Some(injected) = test_machine::injected() {
return injected;
}
static MACHINE_ID: OnceLock<String> = OnceLock::new();
return MACHINE_ID
.get_or_init(read_machine_id)
.as_str();
}
#[cfg(test)]
#[path = "claim_time_machine_id_test_support.rs"]
pub(crate) mod test_machine;
impl ClaimTime {
pub fn read() -> ClaimTime {
let monotonic_millis: u64 = capture_stamp() / 1_000_000;
let boot_id: String = if monotonic_millis == 0 { String::new() } else { String::from(current_boot_id()) };
let wall_seconds: u64 = seconds_since_epoch(SystemTime::now());
return ClaimTime { machine_id: String::from(current_machine_id()), boot_id, monotonic_millis, wall_seconds };
}
pub fn from_parts(boot_id: &str, monotonic: Duration, wall: Duration) -> ClaimTime {
return ClaimTime {
machine_id: String::new(),
boot_id: boot_id.to_owned(),
monotonic_millis: u64::try_from(monotonic.as_millis()).unwrap_or(u64::MAX),
wall_seconds: wall.as_secs(),
};
}
pub fn advanced(&self, by: Duration) -> ClaimTime {
return ClaimTime {
machine_id: self.machine_id.clone(),
boot_id: self.boot_id.clone(),
monotonic_millis: self.monotonic_millis.saturating_add(u64::try_from(by.as_millis()).unwrap_or(u64::MAX)),
wall_seconds: self.wall_seconds.saturating_add(by.as_secs()),
};
}
pub fn rewound(&self, by: Duration) -> ClaimTime {
return ClaimTime {
machine_id: self.machine_id.clone(),
boot_id: self.boot_id.clone(),
monotonic_millis: self.monotonic_millis.saturating_sub(u64::try_from(by.as_millis()).unwrap_or(u64::MAX)),
wall_seconds: self.wall_seconds.saturating_sub(by.as_secs()),
};
}
pub fn wall_stepped_forward(&self, by: Duration) -> ClaimTime {
return ClaimTime { wall_seconds: self.wall_seconds.saturating_add(by.as_secs()), ..self.clone() };
}
pub fn wall_stepped_back(&self, by: Duration) -> ClaimTime {
return ClaimTime { wall_seconds: self.wall_seconds.saturating_sub(by.as_secs()), ..self.clone() };
}
pub fn in_boot(&self, boot_id: &str) -> ClaimTime {
return ClaimTime { boot_id: boot_id.to_owned(), ..self.clone() };
}
pub fn on_machine(&self, machine_id: &str) -> ClaimTime {
return ClaimTime { machine_id: machine_id.to_owned(), ..self.clone() };
}
pub fn from_wall(wall: SystemTime) -> ClaimTime {
return ClaimTime {
machine_id: String::new(),
boot_id: String::new(),
monotonic_millis: 0,
wall_seconds: seconds_since_epoch(wall),
};
}
pub fn elapsed_since(&self, earlier: &ClaimTime) -> Duration {
if !self.boot_id.is_empty() && self.boot_id == earlier.boot_id {
return Duration::from_millis(self.monotonic_millis.saturating_sub(earlier.monotonic_millis));
}
return Duration::from_secs(self.wall_seconds.saturating_sub(earlier.wall_seconds));
}
pub fn is_past_the_boot_of(&self, earlier: &ClaimTime) -> bool {
let same_known_machine: bool = !self.machine_id.is_empty() && self.machine_id == earlier.machine_id;
return same_known_machine && !self.boot_id.is_empty() && !earlier.boot_id.is_empty() && self.boot_id != earlier.boot_id;
}
pub fn encode(&self) -> String {
let without_machine: String = format!("{}{FIELD_SEPARATOR}{}{FIELD_SEPARATOR}{}", self.boot_id, self.monotonic_millis, self.wall_seconds);
if self.machine_id.is_empty() {
return without_machine;
}
return format!("{without_machine}{FIELD_SEPARATOR}{}", self.machine_id);
}
pub fn decode(text: &str) -> Option<ClaimTime> {
let parts: Vec<&str> = text.split(FIELD_SEPARATOR).collect();
if parts.len() != PARTS_WITHOUT_MACHINE && parts.len() != PARTS_WITH_MACHINE {
return None;
}
let boot_id: &str = parts[0];
let monotonic_millis: u64 = parts[1].parse().ok()?;
let wall_seconds: u64 = parts[2].parse().ok()?;
let machine_id: &str = parts.get(PARTS_WITH_MACHINE - 1).copied().unwrap_or("");
let boot_id_fits: bool = boot_id.is_empty() || is_compact_hex_id(boot_id);
let machine_id_fits: bool = parts.len() == PARTS_WITHOUT_MACHINE || is_compact_hex_id(machine_id);
if !boot_id_fits || !machine_id_fits {
return None;
}
return Some(ClaimTime { machine_id: machine_id.to_owned(), boot_id: boot_id.to_owned(), monotonic_millis, wall_seconds });
}
}
#[cfg(test)]
#[path = "claim_time_tests.rs"]
mod tests;