Skip to main content

journal_host/
lib.rs

1//! Optional local-host identity helpers for journal writers.
2//!
3//! This crate is intentionally separate from the core file-format writer. It
4//! probes the local host only when callers explicitly opt in and then pass the
5//! returned values to the writer themselves.
6
7#[cfg(any(target_os = "freebsd", windows, test))]
8mod state;
9
10#[cfg(target_os = "linux")]
11#[path = "platform/linux.rs"]
12mod platform;
13#[cfg(target_os = "freebsd")]
14#[path = "platform/freebsd.rs"]
15mod platform;
16#[cfg(target_os = "macos")]
17#[path = "platform/macos.rs"]
18mod platform;
19#[cfg(windows)]
20#[path = "platform/windows.rs"]
21mod platform;
22#[cfg(not(any(
23    target_os = "linux",
24    target_os = "freebsd",
25    target_os = "macos",
26    windows
27)))]
28#[path = "platform/other.rs"]
29mod platform;
30
31use journal_common::EntryTimestamps;
32use std::io;
33use std::path::PathBuf;
34use std::sync::{Arc, Mutex};
35use std::time::{SystemTime, UNIX_EPOCH};
36use uuid::Uuid;
37
38/// Classifies how the helper obtained the boot ID.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum BootIdSource {
41    /// No boot ID has been loaded yet.
42    Unknown,
43    /// A native per-boot kernel/OS UUID was read.
44    Native,
45    /// A state-backed boot ID was read from or written to healthy state.
46    StateBacked,
47    /// A valid fresh UUID was generated after native/state discovery failed.
48    ///
49    /// Degraded IDs satisfy the strict writer contract for this provider
50    /// instance, but they do not claim cross-process same-boot stability.
51    Degraded,
52}
53
54impl BootIdSource {
55    pub fn as_str(self) -> &'static str {
56        match self {
57            Self::Unknown => "unknown",
58            Self::Native => "native",
59            Self::StateBacked => "state-backed",
60            Self::Degraded => "degraded",
61        }
62    }
63}
64
65/// Diagnostics for the values returned by [`LocalJournalProvider`].
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Diagnostics {
68    pub machine_id_source: String,
69    pub boot_id_source: BootIdSource,
70    pub boot_id_path: Option<PathBuf>,
71    pub monotonic_source: String,
72    pub monotonic_source_detail: String,
73    pub degraded_reason: Option<String>,
74}
75
76impl Default for Diagnostics {
77    fn default() -> Self {
78        Self {
79            machine_id_source: String::new(),
80            boot_id_source: BootIdSource::Unknown,
81            boot_id_path: None,
82            monotonic_source: String::new(),
83            monotonic_source_detail: String::new(),
84            degraded_reason: None,
85        }
86    }
87}
88
89/// Configures local-host helper loading.
90///
91/// The zero value uses platform defaults. State paths are used only on Windows
92/// and on FreeBSD when native `kern.boot_id` is unavailable.
93#[derive(Clone, Default)]
94pub struct LoadOptions {
95    pub state_dir: Option<PathBuf>,
96    pub state_file_name: Option<String>,
97    pub state_path: Option<PathBuf>,
98    pub host_filesystem_prefix: Option<PathBuf>,
99    pub monotonic_now: Option<Arc<dyn Fn() -> io::Result<u64> + Send + Sync>>,
100    pub monotonic_label: Option<String>,
101}
102
103impl LoadOptions {
104    pub fn with_state_dir(mut self, path: impl Into<PathBuf>) -> Self {
105        self.state_dir = Some(path.into());
106        self
107    }
108
109    pub fn with_state_file_name(mut self, name: impl Into<String>) -> Self {
110        self.state_file_name = Some(name.into());
111        self
112    }
113
114    pub fn with_state_path(mut self, path: impl Into<PathBuf>) -> Self {
115        self.state_path = Some(path.into());
116        self
117    }
118
119    /// Resolves Linux host identity from a mounted host filesystem prefix.
120    ///
121    /// This is opt-in. When set on Linux, machine-id resolution checks
122    /// `<prefix>/etc/machine-id` and `<prefix>/var/lib/dbus/machine-id` before
123    /// the container-local paths, and boot-id resolution checks
124    /// `<prefix>/proc/sys/kernel/random/boot_id` before the container-local path.
125    pub fn with_host_filesystem_prefix(mut self, path: impl Into<PathBuf>) -> Self {
126        self.host_filesystem_prefix = Some(path.into());
127        self
128    }
129
130    pub fn with_monotonic_now(
131        mut self,
132        label: impl Into<String>,
133        source: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
134    ) -> Self {
135        self.monotonic_label = Some(label.into());
136        self.monotonic_now = Some(source);
137        self
138    }
139
140    #[cfg(any(target_os = "freebsd", windows))]
141    pub(crate) fn resolve_state_path(
142        &self,
143        default_dir: impl AsRef<std::path::Path>,
144        default_file_name: impl AsRef<str>,
145    ) -> PathBuf {
146        if let Some(path) = &self.state_path {
147            return path.clone();
148        }
149        let dir = self
150            .state_dir
151            .clone()
152            .unwrap_or_else(|| default_dir.as_ref().to_path_buf());
153        let file_name = self
154            .state_file_name
155            .clone()
156            .unwrap_or_else(|| default_file_name.as_ref().to_string());
157        dir.join(file_name)
158    }
159}
160
161/// Local-host values that callers may pass explicitly to the writer.
162pub struct LocalJournalProvider {
163    machine_id: Uuid,
164    boot_id: Uuid,
165    diagnostics: Diagnostics,
166    monotonic_now: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
167    monotonic_label: String,
168    last_monotonic: Mutex<u64>,
169}
170
171impl LocalJournalProvider {
172    pub fn load(options: LoadOptions) -> io::Result<Self> {
173        platform::load(options)
174    }
175
176    pub fn machine_id(&self) -> Uuid {
177        self.machine_id
178    }
179
180    pub fn boot_id(&self) -> Uuid {
181        self.boot_id
182    }
183
184    pub fn diagnostics(&self) -> &Diagnostics {
185        &self.diagnostics
186    }
187
188    pub fn monotonic_source(&self) -> &str {
189        &self.monotonic_label
190    }
191
192    pub fn realtime_usec(&self) -> io::Result<u64> {
193        realtime_usec()
194    }
195
196    /// Returns a boot-anchored monotonic microsecond timestamp.
197    ///
198    /// Values are clamped to strictly increase within this provider instance.
199    pub fn monotonic_usec(&self) -> io::Result<u64> {
200        let mut now = (self.monotonic_now)()?;
201        let mut last = self
202            .last_monotonic
203            .lock()
204            .map_err(|_| io::Error::other("monotonic mutex poisoned"))?;
205        if now <= *last {
206            now = last
207                .checked_add(1)
208                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "monotonic overflow"))?;
209        }
210        *last = now;
211        Ok(now)
212    }
213
214    /// Builds the high-level Rust log-writer timestamp options.
215    pub fn entry_timestamps(&self) -> io::Result<EntryTimestamps> {
216        Ok(EntryTimestamps::default()
217            .with_entry_realtime_usec(self.realtime_usec()?)
218            .with_entry_monotonic_usec(self.monotonic_usec()?))
219    }
220
221    pub(crate) fn new(
222        machine_id: Uuid,
223        boot_id: Uuid,
224        diagnostics: Diagnostics,
225        monotonic_label: impl Into<String>,
226        monotonic_now: Arc<dyn Fn() -> io::Result<u64> + Send + Sync>,
227    ) -> Self {
228        let monotonic_label = monotonic_label.into();
229        let mut diagnostics = diagnostics;
230        diagnostics.monotonic_source = monotonic_label.clone();
231        if diagnostics.monotonic_source_detail.is_empty() {
232            diagnostics.monotonic_source_detail = monotonic_label.clone();
233        }
234        Self {
235            machine_id,
236            boot_id,
237            diagnostics,
238            monotonic_now,
239            monotonic_label,
240            last_monotonic: Mutex::new(0),
241        }
242    }
243}
244
245/// Loads local-host identity and clock values using platform defaults.
246pub fn load(options: LoadOptions) -> io::Result<LocalJournalProvider> {
247    LocalJournalProvider::load(options)
248}
249
250/// Loads local-host identity and panics on failure.
251pub fn must_load(options: LoadOptions) -> LocalJournalProvider {
252    load(options).unwrap_or_else(|err| panic!("journal host helper failed: {err}"))
253}
254
255pub(crate) fn realtime_usec() -> io::Result<u64> {
256    let duration = SystemTime::now()
257        .duration_since(UNIX_EPOCH)
258        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
259    u64::try_from(duration.as_micros())
260        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "realtime microseconds overflow"))
261}
262
263pub(crate) fn parse_uuid_text(text: &str) -> io::Result<Uuid> {
264    Uuid::try_parse(text.trim())
265        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
266        .and_then(reject_zero_uuid)
267}
268
269pub(crate) fn reject_zero_uuid(id: Uuid) -> io::Result<Uuid> {
270    if id.is_nil() {
271        return Err(io::Error::new(
272            io::ErrorKind::InvalidData,
273            "uuid is all zeros",
274        ));
275    }
276    Ok(id)
277}
278
279#[cfg(any(target_os = "freebsd", windows, test))]
280pub(crate) fn uuid_compact(id: Uuid) -> String {
281    id.as_simple().to_string()
282}
283
284#[cfg(unix)]
285pub(crate) fn clock_gettime_usec(clock_id: libc::clockid_t) -> io::Result<u64> {
286    let mut ts = std::mem::MaybeUninit::<libc::timespec>::uninit();
287    // SAFETY: `ts` points to valid storage for clock_gettime to initialize.
288    // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage
289    let rc = unsafe { libc::clock_gettime(clock_id, ts.as_mut_ptr()) };
290    if rc != 0 {
291        return Err(io::Error::last_os_error());
292    }
293    // SAFETY: clock_gettime succeeded and initialized the whole timespec.
294    // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage
295    let ts = unsafe { ts.assume_init() };
296    let sec = u64::try_from(ts.tv_sec).map_err(|_| {
297        io::Error::new(
298            io::ErrorKind::InvalidData,
299            "clock_gettime returned negative seconds",
300        )
301    })?;
302    let nsec = u64::try_from(ts.tv_nsec).map_err(|_| {
303        io::Error::new(
304            io::ErrorKind::InvalidData,
305            "clock_gettime returned negative nanoseconds",
306        )
307    })?;
308    sec.checked_mul(1_000_000)
309        .and_then(|value| value.checked_add(nsec / 1_000))
310        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "clock microseconds overflow"))
311}
312
313#[cfg(any(target_os = "freebsd", target_os = "macos"))]
314pub(crate) fn sysctl_string(name: &str) -> io::Result<String> {
315    let raw = sysctl_raw(name)?;
316    let end = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len());
317    String::from_utf8(raw[..end].to_vec())
318        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
319}
320
321#[cfg(any(target_os = "freebsd", target_os = "macos"))]
322pub(crate) fn sysctl_raw(name: &str) -> io::Result<Vec<u8>> {
323    let name = std::ffi::CString::new(name)
324        .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
325    let mut len = 0usize;
326    // SAFETY: the name is NUL-terminated and len points to valid storage.
327    // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage
328    let rc = unsafe {
329        libc::sysctlbyname(
330            name.as_ptr(),
331            std::ptr::null_mut(),
332            &mut len,
333            std::ptr::null_mut(),
334            0,
335        )
336    };
337    if rc != 0 {
338        return Err(io::Error::last_os_error());
339    }
340    let mut buf = vec![0u8; len];
341    // SAFETY: the output buffer is valid for len bytes and sysctl writes into it.
342    // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage
343    let rc = unsafe {
344        libc::sysctlbyname(
345            name.as_ptr(),
346            buf.as_mut_ptr().cast(),
347            &mut len,
348            std::ptr::null_mut(),
349            0,
350        )
351    };
352    if rc != 0 {
353        return Err(io::Error::last_os_error());
354    }
355    buf.truncate(len);
356    Ok(buf)
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use std::sync::atomic::{AtomicU64, Ordering};
363
364    #[test]
365    fn provider_monotonic_is_strictly_increasing() {
366        let counter = Arc::new(AtomicU64::new(5));
367        let source = {
368            let counter = Arc::clone(&counter);
369            Arc::new(move || Ok(counter.load(Ordering::SeqCst)))
370        };
371        let provider = LocalJournalProvider::new(
372            Uuid::from_u128(1),
373            Uuid::from_u128(2),
374            Diagnostics::default(),
375            "test",
376            source,
377        );
378        assert_eq!(provider.monotonic_usec().unwrap(), 5);
379        assert_eq!(provider.monotonic_usec().unwrap(), 6);
380        counter.store(10, Ordering::SeqCst);
381        assert_eq!(provider.monotonic_usec().unwrap(), 10);
382    }
383}