use crate::rollback::epoch_days;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Policy {
pub months: u32,
}
impl Policy {
pub fn for_channel(channel: &str) -> Option<Policy> {
match channel {
"rolling" => Some(Policy { months: 6 }),
"qualified" => Some(Policy { months: 24 }),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SupportError {
UnknownChannel(String),
BadDate { field: &'static str, value: String },
}
impl fmt::Display for SupportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SupportError::UnknownChannel(c) => write!(
f,
"no support policy is stated for channel `{c}` (known: rolling, \
qualified). Refusing to invent one: a horizon nobody decided \
is a promise nobody made."
),
SupportError::BadDate { field, value } => write!(
f,
"{field} `{value}` is not a date (expected YYYY-MM-DD). This \
field has never been validated, so an unparseable value would \
sign cleanly and then be unusable by everything that reads it."
),
}
}
}
impl std::error::Error for SupportError {}
pub fn horizon(issued_at: &str, channel: &str) -> Result<String, SupportError> {
let policy =
Policy::for_channel(channel).ok_or_else(|| SupportError::UnknownChannel(channel.into()))?;
let date = issued_at.split_once('T').map_or(issued_at, |(d, _)| d);
if epoch_days(date).is_none() {
return Err(SupportError::BadDate {
field: "issued-at",
value: issued_at.to_string(),
});
}
let y: i64 = date[0..4].parse().expect("checked by epoch_days");
let m: i64 = date[5..7].parse().expect("checked by epoch_days");
let d: i64 = date[8..10].parse().expect("checked by epoch_days");
let total = (m - 1) + i64::from(policy.months);
let (ny, nm) = (y + total / 12, total % 12 + 1);
let nd = d.min(days_in_month(ny, nm));
Ok(format!("{ny:04}-{nm:02}-{nd:02}"))
}
fn days_in_month(y: i64, m: i64) -> i64 {
let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ => {
if leap {
29
} else {
28
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Standing {
Supported { days_left: i64 },
Expired { days_ago: i64 },
}
impl Standing {
pub fn is_expired(self) -> bool {
matches!(self, Standing::Expired { .. })
}
}
pub fn standing(support_until: &str, today: &str) -> Result<Standing, SupportError> {
let until = epoch_days(support_until).ok_or_else(|| SupportError::BadDate {
field: "support-until",
value: support_until.to_string(),
})?;
let now = epoch_days(today).ok_or_else(|| SupportError::BadDate {
field: "today",
value: today.to_string(),
})?;
if now <= until {
Ok(Standing::Supported {
days_left: until - now,
})
} else {
Ok(Standing::Expired {
days_ago: now - until,
})
}
}
pub fn advisory(layer: &str, support_until: &str, standing: Standing) -> String {
match standing {
Standing::Expired { days_ago } => format!(
"layer {layer} passed its stated support window on {support_until}, \
{days_ago} day(s) ago. It still installs and still verifies — \
nothing about the bytes has changed. What has changed is that no \
one has undertaken to publish advisories or fixes for it, so a \
problem found tomorrow will not be announced against this layer. \
Move to a supported layer when you can."
),
Standing::Supported { days_left } if days_left <= 30 => format!(
"layer {layer} is supported until {support_until}, {days_left} \
day(s) from now."
),
Standing::Supported { days_left } => {
format!("layer {layer} is supported until {support_until} ({days_left} days)")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_horizon_is_derived_from_the_channel_not_typed_by_hand() {
assert_eq!(
horizon("2026-09-03T00:00:00Z", "rolling").unwrap(),
"2027-03-03"
);
assert_eq!(
horizon("2026-09-03T00:00:00Z", "qualified").unwrap(),
"2028-09-03"
);
assert_eq!(horizon("2026-09-03", "rolling").unwrap(), "2027-03-03");
}
#[test]
fn a_horizon_that_would_fall_on_a_day_that_does_not_exist_is_clamped() {
assert_eq!(horizon("2026-08-31", "rolling").unwrap(), "2027-02-28");
assert_eq!(horizon("2027-08-31", "rolling").unwrap(), "2028-02-29");
assert_eq!(horizon("2026-10-31", "rolling").unwrap(), "2027-04-30");
for d in ["2026-08-31", "2026-10-31", "2027-08-31", "2026-12-31"] {
let h = horizon(d, "rolling").unwrap();
assert!(epoch_days(&h).is_some(), "{d} -> {h} does not parse");
}
}
#[test]
fn a_thirty_one_day_target_month_keeps_all_thirty_one_days() {
assert_eq!(horizon("2026-01-31", "rolling").unwrap(), "2026-07-31");
assert_eq!(horizon("2026-03-31", "rolling").unwrap(), "2026-09-30");
assert_eq!(horizon("2026-07-31", "rolling").unwrap(), "2027-01-31");
}
#[test]
fn february_follows_the_whole_gregorian_leap_rule() {
assert_eq!(horizon("2023-08-31", "rolling").unwrap(), "2024-02-29");
assert_eq!(horizon("2099-08-31", "rolling").unwrap(), "2100-02-28");
assert_eq!(horizon("1999-08-31", "rolling").unwrap(), "2000-02-29");
assert_eq!(horizon("2025-08-31", "rolling").unwrap(), "2026-02-28");
}
#[test]
fn a_supported_layer_does_not_report_itself_expired() {
assert!(!standing("2027-03-03", "2026-09-03").unwrap().is_expired());
assert!(!standing("2026-09-03", "2026-09-03").unwrap().is_expired());
assert!(standing("2026-09-02", "2026-09-03").unwrap().is_expired());
}
#[test]
fn the_year_rolls_over_correctly() {
assert_eq!(horizon("2026-12-15", "rolling").unwrap(), "2027-06-15");
assert_eq!(horizon("2026-07-01", "rolling").unwrap(), "2027-01-01");
assert_eq!(horizon("2026-01-15", "qualified").unwrap(), "2028-01-15");
}
#[test]
fn an_unknown_channel_gets_no_horizon_rather_than_a_guessed_one() {
let e = horizon("2026-09-03", "experimental").expect_err("must refuse");
assert!(matches!(e, SupportError::UnknownChannel(_)), "{e:?}");
assert!(e.to_string().contains("a promise nobody made"), "{e}");
assert_eq!(Policy::for_channel("nope"), None);
}
#[test]
fn an_unparseable_horizon_is_refused_rather_than_compared() {
for bad in [
"",
"next year",
"2028-13-45",
"2028-02-30",
"28-01-01",
"2028/01/01",
] {
let e = standing(bad, "2026-09-03").expect_err(bad);
assert!(matches!(e, SupportError::BadDate { .. }), "{bad}: {e:?}");
}
assert!(horizon("not-a-date", "rolling").is_err());
}
#[test]
fn the_stated_day_is_still_supported_and_the_next_one_is_not() {
assert_eq!(
standing("2027-03-03", "2027-03-03").unwrap(),
Standing::Supported { days_left: 0 }
);
assert_eq!(
standing("2027-03-03", "2027-03-04").unwrap(),
Standing::Expired { days_ago: 1 }
);
assert_eq!(
standing("2027-03-03", "2027-03-02").unwrap(),
Standing::Supported { days_left: 1 }
);
}
#[test]
fn an_expired_layer_is_explained_rather_than_refused() {
let s = standing("2026-01-01", "2026-09-03").unwrap();
assert!(s.is_expired());
let msg = advisory("2026.01.0", "2026-01-01", s);
assert!(msg.contains("still installs and still verifies"), "{msg}");
assert!(msg.contains("245 day(s) ago"), "{msg}");
assert!(msg.contains("advisories"), "{msg}");
}
#[test]
fn a_window_closing_soon_reads_differently_from_one_far_off() {
let soon = advisory(
"x",
"2026-09-20",
standing("2026-09-20", "2026-09-03").unwrap(),
);
assert!(soon.contains("17 day(s) from now"), "{soon}");
let far = advisory(
"x",
"2027-09-20",
standing("2027-09-20", "2026-09-03").unwrap(),
);
assert!(far.contains("(382 days)"), "{far}");
}
}