use std::fmt;
use crate::constants::{
MAX_IMAGE_HOOK_TIMEOUT_SEC, MAX_MICROVM_HOOK_TIMEOUT_SEC, MODEL_API_VERSION,
};
use crate::error::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RunHookTimeout(u32);
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct BuildHookTimeout(u32);
impl RunHookTimeout {
pub const MAX_SECS: u32 = MAX_MICROVM_HOOK_TIMEOUT_SEC;
pub fn try_new(secs: u32) -> Result<Self, Error> {
if (1..=Self::MAX_SECS).contains(&secs) {
return Ok(Self(secs));
}
Err(Error::invalid_arg(out_of_range_message(
"microvmHooks",
secs,
Self::MAX_SECS,
BuildHookTimeout::MAX_SECS,
)))
}
pub fn as_secs(self) -> u32 {
self.0
}
}
impl BuildHookTimeout {
pub const MAX_SECS: u32 = MAX_IMAGE_HOOK_TIMEOUT_SEC;
pub fn try_new(secs: u32) -> Result<Self, Error> {
if (1..=Self::MAX_SECS).contains(&secs) {
return Ok(Self(secs));
}
Err(Error::invalid_arg(out_of_range_message(
"microvmImageHooks",
secs,
Self::MAX_SECS,
RunHookTimeout::MAX_SECS,
)))
}
pub fn as_secs(self) -> u32 {
self.0
}
}
impl fmt::Display for RunHookTimeout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}s", self.0)
}
}
impl fmt::Display for BuildHookTimeout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}s", self.0)
}
}
fn out_of_range_message(block: &str, secs: u32, ceiling: u32, other: u32) -> String {
format!(
"{block} timeout of {secs}s is outside the accepted range 1..{ceiling} (service model \
{MODEL_API_VERSION}). The two hook families have ceilings 60x apart — {block} caps at \
{ceiling}s while the other family caps at {other}s — because a build hook waits on a \
Dockerfile and a run hook waits on a daemon that is already booted \
(docs/PLATFORM.md, 'The `runHookPayload` ceiling is 4096 bytes, and the service model \
states it')."
)
}
pub fn require_hook_port(port: u32) -> Result<u16, Error> {
if (1..=u32::from(u16::MAX)).contains(&port) {
return Ok(port as u16);
}
Err(Error::invalid_arg(format!(
"hooks.port={port} is outside 1..{} (service model {MODEL_API_VERSION}).",
u16::MAX
)))
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
use crate::error::ErrorKind;
#[test]
fn the_two_families_cap_sixty_seconds_apart_by_a_factor_of_sixty() {
assert_eq!(RunHookTimeout::MAX_SECS, 60);
assert_eq!(BuildHookTimeout::MAX_SECS, 3600);
assert_eq!(
BuildHookTimeout::MAX_SECS,
RunHookTimeout::MAX_SECS * 60,
"the 60x gap is the finding"
);
}
#[test]
fn each_family_accepts_its_ceiling_and_refuses_one_past_it() {
assert_eq!(RunHookTimeout::try_new(1).expect("1s fits").as_secs(), 1);
assert_eq!(RunHookTimeout::try_new(60).expect("60s fits").as_secs(), 60);
assert!(RunHookTimeout::try_new(0).is_err());
assert!(RunHookTimeout::try_new(61).is_err());
assert_eq!(BuildHookTimeout::try_new(1).expect("1s fits").as_secs(), 1);
assert_eq!(
BuildHookTimeout::try_new(3600)
.expect("3600s fits")
.as_secs(),
3600
);
assert!(BuildHookTimeout::try_new(0).is_err());
assert!(BuildHookTimeout::try_new(3601).is_err());
}
#[test]
fn a_build_sized_timeout_is_refused_by_the_run_family_naming_both_ceilings() {
let build = BuildHookTimeout::try_new(300).expect("300s is legal for a build hook");
assert_eq!(build.as_secs(), 300);
let err = RunHookTimeout::try_new(300).expect_err("300s is not legal for a run hook");
assert_eq!(err.kind(), ErrorKind::InvalidArg);
let message = err.to_string();
assert!(message.contains("microvmHooks"), "{message}");
assert!(message.contains("1..60"), "{message}");
assert!(
message.contains("3600s"),
"must name the other ceiling: {message}"
);
assert!(message.contains("60x apart"), "{message}");
assert!(
message.contains("2025-09-09"),
"must name the model: {message}"
);
}
#[test]
fn the_build_family_refusal_names_the_run_ceiling() {
let err = BuildHookTimeout::try_new(7200).expect_err("7200s is past the image ceiling");
let message = err.to_string();
assert!(message.contains("microvmImageHooks"), "{message}");
assert!(message.contains("1..3600"), "{message}");
assert!(
message.contains("60s"),
"must name the other ceiling: {message}"
);
}
#[test]
fn a_hook_port_outside_the_model_range_is_refused() {
assert_eq!(require_hook_port(1).expect("1 fits"), 1);
assert_eq!(require_hook_port(9000).expect("9000 fits"), 9000);
assert_eq!(require_hook_port(65535).expect("65535 fits"), 65535);
for bad in [0, 65536, u32::MAX] {
let err = require_hook_port(bad).expect_err("outside 1..65535");
assert_eq!(err.kind(), ErrorKind::InvalidArg, "{bad}");
assert!(err.to_string().contains("1..65535"), "{bad}");
}
}
proptest! {
#[test]
fn the_two_ceilings_disagree_across_the_whole_sixty_to_thirty_six_hundred_band(secs: u32) {
let run = RunHookTimeout::try_new(secs);
let build = BuildHookTimeout::try_new(secs);
prop_assert_eq!(run.is_ok(), (1..=60).contains(&secs));
prop_assert_eq!(build.is_ok(), (1..=3600).contains(&secs));
if (61..=3600).contains(&secs) {
prop_assert!(build.is_ok(), "{} is a legal build timeout", secs);
let err = run.expect_err("but not a legal run timeout");
prop_assert_eq!(err.kind(), ErrorKind::InvalidArg);
}
if let Ok(run) = RunHookTimeout::try_new(secs) {
prop_assert_eq!(run.as_secs(), secs);
}
if let Ok(build) = BuildHookTimeout::try_new(secs) {
prop_assert_eq!(build.as_secs(), secs);
}
}
}
}