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.bytes().all(|byte| { return byte.is_ascii_hexdigit(); });
}
pub(crate) fn is_machine_id(text: &str) -> bool {
return text.len() == BOOT_ID_LENGTH
&& text.bytes().all(|byte| { return byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte); })
&& text.bytes().any(|byte| { return byte != b'0'; });
}
pub(crate) fn current_boot_id() -> &'static str {
static BOOT_ID: OnceLock<String> = OnceLock::new();
return BOOT_ID
.get_or_init(|| {
let Ok(text) = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") else {
return String::new();
};
let compact: String = text.trim().chars().filter(|character| { return *character != '-'; }).collect();
return if is_compact_hex_id(compact.as_str()) { compact } else { String::new() };
})
.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(|| {
let paths: Vec<&Path> = MACHINE_ID_PATHS.iter().map(|path| { return Path::new(*path); }).collect();
return machine_id_from(paths.as_slice());
})
.as_str();
}
#[cfg(test)]
pub(crate) mod test_machine {
use std::cell::Cell;
thread_local! {
static INJECTED: Cell<Option<&'static str>> = const { Cell::new(None) };
}
pub(crate) fn injected() -> Option<&'static str> {
return INJECTED.with(Cell::get);
}
pub(crate) fn inject(id: &'static str) {
INJECTED.with(|cell| { cell.set(Some(id)); });
}
}
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 = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |elapsed| { return elapsed.as_secs(); });
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: wall.duration_since(UNIX_EPOCH).map_or(0, |elapsed| { return elapsed.as_secs(); }),
};
}
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;