use std::time::Duration; use anyhow::{anyhow, bail, ensure, Context}; use http::HeaderValue; use crate::error::Error; // Timeout-Milliseconds → {positive integer as ASCII string of at most 10 digits} const CONNECT_TIMEOUT_MAX: Duration = Duration::from_millis(9_999_999_999); // Note: The spec says "positive integer" but zero seems to be supported. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ConnectTimeout(Duration); impl ConnectTimeout { pub fn from_millis(timeout_ms: u32) -> Self { Self(Duration::from_millis(timeout_ms.into())) } pub fn clamp(timeout: Duration) -> Self { Self(timeout.clamp(Duration::ZERO, CONNECT_TIMEOUT_MAX)) } pub fn from_connect_timeout(header_value: impl AsRef<[u8]>) -> Result { fn parse(value: &[u8]) -> anyhow::Result { Ok(std::str::from_utf8(value)?.parse()?) } let value = parse(header_value.as_ref()) .context("invalid connect timeout") .map_err(Error::InvalidTimeout)?; Duration::from_millis(value.into()).try_into() } pub fn from_grpc_timeout(header_value: impl AsRef<[u8]>) -> Result { let timeout = (|| { let (unit, value) = header_value.as_ref().split_last().context("too short")?; ensure!(value.len() <= 8, "too long"); let value: u64 = std::str::from_utf8(value)?.parse()?; Ok(match char::from(*unit) { 'H' => Duration::from_secs(value * 3600), 'M' => Duration::from_secs(value * 60), 'S' => Duration::from_secs(value), 'm' => Duration::from_millis(value), 'u' => Duration::from_micros(value), 'n' => Duration::from_nanos(value), other => { bail!("invalid unit {other:?}"); } }) })() .with_context(|| { format!( "invalid grpc-timeout value \"{}\"", header_value.as_ref().escape_ascii() ) }) .map_err(Error::InvalidTimeout)?; Ok(Self::clamp(timeout)) } pub fn to_connect_timeout(&self) -> HeaderValue { self.0 .as_millis() .to_string() .try_into() .expect("timeout is ascii") } } impl std::ops::Deref for ConnectTimeout { type Target = Duration; fn deref(&self) -> &Self::Target { &self.0 } } impl TryFrom for ConnectTimeout { type Error = Error; fn try_from(value: Duration) -> Result { if value > CONNECT_TIMEOUT_MAX { return Err(Error::InvalidTimeout(anyhow!( "{value:?} > max {CONNECT_TIMEOUT_MAX:?}" ))); } Ok(Self(value)) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_grpc_to_connect_timeout() -> anyhow::Result<()> { for (grpc_timeout, connect_timeout) in [ ("2H", "7200000"), ("30M", "1800000"), ("4000S", "4000000"), ("50000m", "50000"), ("2200u", "2"), ("9n", "0"), ] { let grpc = ConnectTimeout::from_grpc_timeout(grpc_timeout)?; let connect = ConnectTimeout::from_connect_timeout(connect_timeout)?; assert_eq!( grpc, connect, "grpc-timeout = {grpc_timeout:?}, connect-timeout-ms = {connect_timeout:?}" ); } Ok(()) } }