///|
/// Seconds in a day. Leap seconds are not subtracted: a freshness window is a
/// tolerance measured in days, and being a second out over three decades does
/// not change any decision it makes.
let seconds_per_day = 86400L
///|
/// The earliest publication instant this client will believe, from the system
/// clock.
///
/// Kept out of `UpdateChannel::check` on purpose. That function is the one
/// sequence where the order of the steps is the security property, and a clock
/// read inside it would make the whole sequence depend on when the test ran.
/// Here the clock is read once and the result is data.
pub fn oldest_accepted(
freshness_days : Int,
) -> @manifest.Timestamp raise UpdateError {
oldest_accepted_at(@env.now(), freshness_days)
}
///|
/// The same, from a clock reading supplied by the caller.
fn oldest_accepted_at(
now_ms : UInt64,
freshness_days : Int,
) -> @manifest.Timestamp raise UpdateError {
guard freshness_days > 0 else {
raise ClockUnusable(
detail="a freshness window of \{freshness_days} days accepts nothing",
)
}
let now_seconds = (now_ms / 1000).reinterpret_as_int64()
let oldest = now_seconds - freshness_days.to_int64() * seconds_per_day
guard oldest >= 0L else {
raise ClockUnusable(
detail="the system clock reads \{now_seconds}, before the epoch once the freshness window is subtracted",
)
}
timestamp_of_unix_second(oldest)
}
///|
/// Renders a Unix instant as the fixed-width UTC text a `Timestamp` accepts.
///
/// The text is built here and parsed back rather than constructed directly,
/// so the one validator both sides of the comparison went through is the same
/// one. A timestamp this client invents and a timestamp a publisher wrote must
/// be the same kind of thing, or ordering them means nothing.
fn timestamp_of_unix_second(
seconds : Int64,
) -> @manifest.Timestamp raise UpdateError {
let moment = @time.ZonedDateTime::from_unix_second(
seconds,
zone=@time.utc_zone,
) catch {
error => raise ClockUnusable(detail=@debug.render(Repr(error)))
}
let text = "\{pad(moment.year(), 4)}-\{pad(moment.month(), 2)}-\{pad(moment.day(), 2)}T\{pad(moment.hour(), 2)}:\{pad(moment.minute(), 2)}:\{pad(moment.second(), 2)}Z"
match @manifest.Timestamp::parse(text) {
Some(timestamp) => timestamp
None => raise ClockUnusable(detail="the system clock reads \{text}")
}
}
///|
/// Left-pads a non-negative number with zeros to a fixed width.
///
/// A year before 1000 or after 9999 cannot be written in four digits. Rather
/// than emit a shorter or longer field — which would break the lexicographic
/// ordering the whole timestamp design rests on — it returns something the
/// parser rejects, and the caller reports an unusable clock.
fn pad(value : Int, width : Int) -> String {
let digits = value.to_string()
if digits.length() > width {
return digits
}
let builder = StringBuilder::new(size_hint=width)
for _ in 0..<(width - digits.length()) {
builder.write_char('0')
}
builder.write_string(digits)
builder.to_string()
}