Skip to main content

loftspec_store/
clock.rs

1//! The clock port: the time of a publish, in whole seconds since the Unix epoch, from
2//! `SOURCE_DATE_EPOCH` when it is set and from the system clock otherwise.
3
4use std::ffi::OsString;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::StoreError;
8
9/// The environment variable that names a publish time, for a reproducible publish.
10pub const EPOCH_ENV: &str = "SOURCE_DATE_EPOCH";
11
12/// Where a publish learns the time.
13pub trait Clock {
14    /// The time now, in whole seconds since the Unix epoch.
15    ///
16    /// # Errors
17    ///
18    /// The time cannot be read.
19    fn now(&self) -> Result<u64, StoreError>;
20}
21
22/// The system clock, unless an epoch is given.
23#[derive(Debug, Clone, Default)]
24pub struct SystemClock {
25    epoch: Option<OsString>,
26}
27
28impl SystemClock {
29    /// The clock as the process's environment sets it.
30    #[must_use]
31    pub fn from_env() -> Self {
32        Self::with_epoch(std::env::var_os(EPOCH_ENV))
33    }
34
35    /// The clock with `SOURCE_DATE_EPOCH` set to `epoch`, or unset.
36    #[must_use]
37    pub fn with_epoch(epoch: Option<impl Into<OsString>>) -> Self {
38        Self {
39            epoch: epoch.map(Into::into),
40        }
41    }
42}
43
44impl Clock for SystemClock {
45    fn now(&self) -> Result<u64, StoreError> {
46        match &self.epoch {
47            Some(epoch) => epoch
48                .to_str()
49                .filter(|text| !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()))
50                .and_then(|text| text.parse().ok())
51                .ok_or_else(|| StoreError::Epoch(epoch.to_string_lossy().into_owned())),
52            None => Ok(SystemTime::now()
53                .duration_since(UNIX_EPOCH)
54                .map_err(|_| StoreError::Epoch("a system time before 1970".to_owned()))?
55                .as_secs()),
56        }
57    }
58}