use core::fmt;
use core::num::NonZeroU16;
use core::time::Duration;
use std::borrow::Cow;
use thiserror::Error;
use tokio_util::sync::CancellationToken;
use url::Url;
use crate::error_messages;
pub type HttpHeaders = Vec<(String, String)>;
pub type TargetVecResult = crate::Result<Vec<Target>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PortCategory {
System,
User,
Dynamic,
}
impl PortCategory {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::System => "system",
Self::User => "user",
Self::Dynamic => "dynamic",
}
}
#[must_use]
pub const fn range(&self) -> (u16, u16) {
match self {
Self::System => (1, 1023),
Self::User => (1024, 49151),
Self::Dynamic => (49152, 65535),
}
}
}
impl fmt::Display for PortCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Port(NonZeroU16);
impl Port {
#[must_use]
#[inline]
pub const fn new(port: u16) -> Option<Self> {
match NonZeroU16::new(port) {
Some(nz) => Some(Self(nz)),
None => None,
}
}
#[must_use]
#[inline]
pub const fn system_port(port: u16) -> Option<Self> {
if port == 0 || port > 1023 {
None
} else {
Self::new(port)
}
}
#[must_use]
#[inline]
pub const fn user_port(port: u16) -> Option<Self> {
if port < 1024 || port > 49151 {
None
} else {
Self::new(port)
}
}
#[must_use]
#[inline]
pub const fn dynamic_port(port: u16) -> Option<Self> {
if port < 49152 { None } else { Self::new(port) }
}
#[must_use]
#[inline]
pub const fn new_unchecked(port: u16) -> Self {
match NonZeroU16::new(port) {
Some(nz) => Self(nz),
None => {
panic!("Port::new_unchecked called with invalid port 0");
}
}
}
#[must_use]
pub const fn http() -> Self {
Self::new_unchecked(80)
}
#[must_use]
pub const fn https() -> Self {
Self::new_unchecked(443)
}
#[must_use]
pub const fn ssh() -> Self {
Self::new_unchecked(22)
}
#[must_use]
pub const fn postgres() -> Self {
Self::new_unchecked(5432)
}
#[must_use]
pub const fn mysql() -> Self {
Self::new_unchecked(3306)
}
#[must_use]
pub const fn redis() -> Self {
Self::new_unchecked(6379)
}
#[must_use]
#[inline(always)]
pub const fn get(&self) -> u16 {
self.0.get()
}
#[must_use]
#[inline]
pub const fn is_system_port(&self) -> bool {
self.0.get() <= 1023
}
#[must_use]
#[inline]
pub const fn is_user_port(&self) -> bool {
let port = self.0.get();
port >= 1024 && port <= 49151
}
#[must_use]
#[inline]
pub const fn is_dynamic_port(&self) -> bool {
self.0.get() >= 49152
}
#[must_use]
#[inline]
pub const fn category(&self) -> PortCategory {
let port = self.0.get();
if port <= 1023 {
PortCategory::System
} else if port <= 49151 {
PortCategory::User
} else {
PortCategory::Dynamic
}
}
}
impl fmt::Display for Port {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<u16> for Port {
type Error = crate::WaitForError;
fn try_from(port: u16) -> crate::Result<Self> {
Self::new(port).ok_or_else(|| crate::WaitForError::InvalidPort(port))
}
}
impl TryFrom<u32> for Port {
type Error = crate::WaitForError;
fn try_from(port: u32) -> crate::Result<Self> {
if port > u32::from(u16::MAX) {
return Err(crate::WaitForError::InvalidPort(0)); }
Self::try_from(u16::try_from(port).unwrap_or(0))
}
}
impl TryFrom<i32> for Port {
type Error = crate::WaitForError;
fn try_from(port: i32) -> crate::Result<Self> {
if port < 0 || port > i32::from(u16::MAX) {
return Err(crate::WaitForError::InvalidPort(0)); }
Self::try_from(u16::try_from(port).unwrap_or(0))
}
}
impl TryFrom<usize> for Port {
type Error = crate::WaitForError;
fn try_from(port: usize) -> crate::Result<Self> {
if port > usize::from(u16::MAX) {
return Err(crate::WaitForError::InvalidPort(0)); }
Self::try_from(u16::try_from(port).unwrap_or(0))
}
}
impl TryFrom<NonZeroU16> for Port {
type Error = crate::WaitForError;
fn try_from(port: NonZeroU16) -> crate::Result<Self> {
Ok(Self(port))
}
}
impl std::str::FromStr for Port {
type Err = crate::WaitForError;
fn from_str(s: &str) -> crate::Result<Self> {
let port: u16 = s.parse().map_err(|_| crate::WaitForError::InvalidPort(0))?;
Self::try_from(port)
}
}
impl From<Port> for u16 {
#[inline(always)]
fn from(port: Port) -> Self {
port.0.get()
}
}
impl From<Port> for NonZeroU16 {
#[inline(always)]
fn from(port: Port) -> Self {
port.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StatusCode(u16);
impl StatusCode {
#[must_use]
#[inline]
pub const fn new(code: u16) -> Option<Self> {
if code >= 100 && code <= 599 {
Some(Self(code))
} else {
None
}
}
#[must_use]
#[inline(always)]
pub const fn get(&self) -> u16 {
self.0
}
#[must_use]
#[inline]
pub const fn is_success(&self) -> bool {
self.0 >= 200 && self.0 <= 299
}
#[must_use]
#[inline]
pub const fn is_redirection(&self) -> bool {
self.0 >= 300 && self.0 <= 399
}
#[must_use]
#[inline]
pub const fn is_client_error(&self) -> bool {
self.0 >= 400 && self.0 <= 499
}
#[must_use]
#[inline]
pub const fn is_server_error(&self) -> bool {
self.0 >= 500 && self.0 <= 599
}
pub const OK: Self = Self(200);
pub const CREATED: Self = Self(201);
pub const ACCEPTED: Self = Self(202);
pub const NO_CONTENT: Self = Self(204);
pub const MOVED_PERMANENTLY: Self = Self(301);
pub const FOUND: Self = Self(302);
pub const NOT_MODIFIED: Self = Self(304);
pub const BAD_REQUEST: Self = Self(400);
pub const UNAUTHORIZED: Self = Self(401);
pub const FORBIDDEN: Self = Self(403);
pub const NOT_FOUND: Self = Self(404);
pub const INTERNAL_SERVER_ERROR: Self = Self(500);
pub const BAD_GATEWAY: Self = Self(502);
pub const SERVICE_UNAVAILABLE: Self = Self(503);
}
impl fmt::Display for StatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<u16> for StatusCode {
type Error = crate::WaitForError;
fn try_from(code: u16) -> crate::Result<Self> {
Self::new(code).ok_or_else(|| {
crate::WaitForError::InvalidTarget(Cow::Owned(format!(
"Invalid HTTP status code: {code} (must be 100-599)"
)))
})
}
}
impl From<StatusCode> for u16 {
#[inline(always)]
fn from(code: StatusCode) -> Self {
code.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RetryCount(u32);
impl RetryCount {
#[must_use]
#[inline]
pub const fn new(count: u32) -> Self {
Self(count)
}
#[must_use]
#[inline(always)]
pub const fn get(&self) -> u32 {
self.0
}
#[must_use]
#[inline]
pub const fn unlimited() -> Option<Self> {
None
}
pub const FEW: Self = Self(3);
pub const MODERATE: Self = Self(5);
pub const MANY: Self = Self(10);
pub const AGGRESSIVE: Self = Self(20);
}
impl fmt::Display for RetryCount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<u32> for RetryCount {
#[inline(always)]
fn from(count: u32) -> Self {
Self(count)
}
}
impl From<RetryCount> for u32 {
#[inline(always)]
fn from(count: RetryCount) -> Self {
count.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Hostname(Cow<'static, str>);
impl Hostname {
pub fn new(hostname: impl Into<String>) -> crate::Result<Self> {
let hostname = hostname.into();
Self::validate(&hostname)?;
Ok(Self(Cow::Owned(hostname)))
}
#[must_use]
pub const fn from_static(hostname: &'static str) -> Self {
Self(Cow::Borrowed(hostname))
}
fn validate(hostname: &str) -> crate::Result<()> {
if hostname.is_empty() {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::EMPTY_HOSTNAME,
)));
}
if hostname.len() > 253 {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::HOSTNAME_TOO_LONG,
)));
}
if hostname.starts_with('-') || hostname.ends_with('-') {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::HOSTNAME_INVALID_HYPHEN,
)));
}
for label in hostname.split('.') {
if label.is_empty() {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::HOSTNAME_EMPTY_LABEL,
)));
}
if label.len() > 63 {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::HOSTNAME_LABEL_TOO_LONG,
)));
}
if label.starts_with('-') || label.ends_with('-') {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::HOSTNAME_LABEL_INVALID_HYPHEN,
)));
}
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::HOSTNAME_INVALID_CHARS,
)));
}
}
Ok(())
}
#[must_use]
pub const fn localhost() -> Self {
Self::from_static("localhost")
}
#[must_use]
pub const fn loopback() -> Self {
Self::from_static("127.0.0.1")
}
#[must_use]
pub const fn loopback_v6() -> Self {
Self::from_static("::1")
}
#[must_use]
pub const fn any() -> Self {
Self::from_static("0.0.0.0")
}
pub fn ipv4(ip: impl AsRef<str>) -> crate::Result<Self> {
let ip = ip.as_ref();
let mut parts_count = 0;
for part in ip.split('.') {
parts_count += 1;
if parts_count > 4 {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::INVALID_IPV4_FORMAT,
)));
}
let _num: u8 = part.parse().map_err(|_| {
crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::INVALID_IPV4_OCTET,
))
})?;
}
if parts_count != 4 {
return Err(crate::WaitForError::InvalidHostname(Cow::Borrowed(
error_messages::INVALID_IPV4_FORMAT,
)));
}
Ok(Self(Cow::Owned(ip.to_string())))
}
#[must_use]
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
#[inline]
pub fn is_localhost(&self) -> bool {
self.0 == "localhost"
}
#[must_use]
#[inline]
pub fn is_loopback(&self) -> bool {
self.0 == "127.0.0.1" || self.0 == "::1"
}
#[must_use]
#[inline]
pub fn is_ipv4(&self) -> bool {
let parts: Vec<&str> = self.0.split('.').collect();
parts.len() == 4 && parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()))
}
#[must_use]
#[inline]
pub fn is_ipv6(&self) -> bool {
self.0.contains(':')
}
#[must_use]
pub const fn ipv4_loopback() -> Self {
Self::from_static("127.0.0.1")
}
#[must_use]
pub const fn ipv6_loopback() -> Self {
Self::from_static("::1")
}
#[must_use]
pub const fn ipv4_any() -> Self {
Self::from_static("0.0.0.0")
}
#[must_use]
pub const fn ipv6_any() -> Self {
Self::from_static("::")
}
}
impl fmt::Display for Hostname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<String> for Hostname {
type Error = crate::WaitForError;
fn try_from(hostname: String) -> crate::Result<Self> {
Self::new(hostname)
}
}
impl TryFrom<&str> for Hostname {
type Error = crate::WaitForError;
fn try_from(hostname: &str) -> crate::Result<Self> {
Self::new(hostname)
}
}
impl std::str::FromStr for Hostname {
type Err = crate::WaitForError;
fn from_str(s: &str) -> crate::Result<Self> {
Self::try_from(s)
}
}
impl TryFrom<std::net::IpAddr> for Hostname {
type Error = crate::WaitForError;
fn try_from(ip: std::net::IpAddr) -> crate::Result<Self> {
match ip {
std::net::IpAddr::V4(ipv4) => Self::ipv4(ipv4.to_string()),
std::net::IpAddr::V6(ipv6) => Self::new(ipv6.to_string()),
}
}
}
impl TryFrom<std::net::Ipv4Addr> for Hostname {
type Error = crate::WaitForError;
fn try_from(ip: std::net::Ipv4Addr) -> crate::Result<Self> {
Self::ipv4(ip.to_string())
}
}
impl TryFrom<std::net::Ipv6Addr> for Hostname {
type Error = crate::WaitForError;
fn try_from(ip: std::net::Ipv6Addr) -> crate::Result<Self> {
Self::new(ip.to_string())
}
}
impl AsRef<str> for Hostname {
#[inline(always)]
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for Hostname {
#[inline(always)]
fn borrow(&self) -> &str {
&self.0
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ConnectionError {
#[error("Failed to connect to {host}:{port} - {reason}")]
TcpConnection {
host: Cow<'static, str>,
port: u16,
#[source]
reason: std::io::Error,
},
#[error("Connection timeout after {timeout_ms}ms")]
Timeout {
timeout_ms: u64,
},
#[error("DNS resolution failed for {host}: {reason}")]
DnsResolution {
host: Cow<'static, str>,
#[source]
reason: std::io::Error,
},
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum HttpError {
#[error("HTTP request failed for {url}: {reason}")]
RequestFailed {
url: Cow<'static, str>,
#[source]
reason: reqwest::Error,
},
#[error("Unexpected status code: expected {expected}, got {actual}")]
UnexpectedStatus {
expected: u16,
actual: u16,
},
#[error("Invalid header: {header}")]
InvalidHeader {
header: Cow<'static, str>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Target {
Tcp {
host: Hostname,
port: Port,
},
Http {
url: Url,
expected_status: u16,
headers: Option<HttpHeaders>,
},
}
impl TryFrom<&str> for Target {
type Error = crate::WaitForError;
fn try_from(target_str: &str) -> crate::Result<Self> {
Self::parse(target_str, 200)
}
}
impl TryFrom<String> for Target {
type Error = crate::WaitForError;
fn try_from(target_str: String) -> crate::Result<Self> {
Self::try_from(target_str.as_str())
}
}
impl std::str::FromStr for Target {
type Err = crate::WaitForError;
fn from_str(s: &str) -> crate::Result<Self> {
Self::try_from(s)
}
}
impl Target {
pub fn try_tcp<H, P>(host: H, port: P) -> crate::Result<Self>
where
H: TryInto<Hostname>,
P: TryInto<Port>,
H::Error: Into<crate::WaitForError>,
P::Error: Into<crate::WaitForError>,
{
let hostname = host.try_into().map_err(Into::into)?;
let port = port.try_into().map_err(Into::into)?;
Ok(Self::Tcp {
host: hostname,
port,
})
}
pub fn try_http(url: impl AsRef<str>, expected_status: u16) -> crate::Result<Self> {
let url = Url::parse(url.as_ref())?;
Ok(Self::Http {
url,
expected_status,
headers: None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ValidatedDuration(Duration);
impl ValidatedDuration {
#[must_use]
pub const fn new(duration: Duration) -> Self {
Self(duration)
}
#[must_use]
pub const fn get(&self) -> Duration {
self.0
}
#[must_use]
pub const fn from_secs(secs: u64) -> Self {
Self(Duration::from_secs(secs))
}
#[must_use]
pub const fn from_millis(millis: u64) -> Self {
Self(Duration::from_millis(millis))
}
}
impl From<ValidatedDuration> for Duration {
fn from(vd: ValidatedDuration) -> Self {
vd.0
}
}
impl TryFrom<Duration> for ValidatedDuration {
type Error = crate::WaitForError;
fn try_from(duration: Duration) -> crate::Result<Self> {
Ok(Self(duration))
}
}
impl std::str::FromStr for ValidatedDuration {
type Err = crate::WaitForError;
fn from_str(s: &str) -> crate::Result<Self> {
let s = s.trim();
if let Ok(secs) = s.parse::<u64>() {
return Ok(Self::from_secs(secs));
}
let (number_part, unit_part) =
if let Some(pos) = s.find(|c: char| !c.is_ascii_digit() && c != '.') {
s.split_at(pos)
} else {
return Err(crate::WaitForError::InvalidTimeout(
Cow::Owned(s.to_string()),
Cow::Borrowed("Invalid duration format"),
));
};
let number: f64 = number_part.parse().map_err(|_| {
crate::WaitForError::InvalidTimeout(
Cow::Owned(s.to_string()),
Cow::Borrowed("Invalid number"),
)
})?;
let duration = match unit_part {
"ms" => {
#[expect(
clippy::cast_precision_loss,
reason = "duration calculation requires f64"
)]
let millis = (number * 1.0).min(u64::MAX as f64);
if millis < 0.0 {
return Err(crate::WaitForError::InvalidTimeout(
Cow::Owned(s.to_string()),
Cow::Borrowed("Duration cannot be negative"),
));
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "safe cast after bounds check"
)]
Duration::from_millis(millis as u64)
}
"s" => {
#[expect(
clippy::cast_precision_loss,
reason = "duration calculation requires f64"
)]
let millis = (number * 1000.0).min(u64::MAX as f64);
if millis < 0.0 {
return Err(crate::WaitForError::InvalidTimeout(
Cow::Owned(s.to_string()),
Cow::Borrowed("Duration cannot be negative"),
));
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "safe cast after bounds check"
)]
Duration::from_millis(millis as u64)
}
"m" => {
#[expect(
clippy::cast_precision_loss,
reason = "duration calculation requires f64"
)]
let millis = (number * 60_000.0).min(u64::MAX as f64);
if millis < 0.0 {
return Err(crate::WaitForError::InvalidTimeout(
Cow::Owned(s.to_string()),
Cow::Borrowed("Duration cannot be negative"),
));
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "safe cast after bounds check"
)]
Duration::from_millis(millis as u64)
}
"h" => {
#[expect(
clippy::cast_precision_loss,
reason = "duration calculation requires f64"
)]
let millis = (number * 3_600_000.0).min(u64::MAX as f64);
if millis < 0.0 {
return Err(crate::WaitForError::InvalidTimeout(
Cow::Owned(s.to_string()),
Cow::Borrowed("Duration cannot be negative"),
));
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "safe cast after bounds check"
)]
Duration::from_millis(millis as u64)
}
_ => {
return Err(crate::WaitForError::InvalidTimeout(
Cow::Owned(s.to_string()),
Cow::Borrowed("Unknown time unit (use: ms, s, m, h)"),
));
}
};
Ok(Self(duration))
}
}
impl TryFrom<&str> for ValidatedDuration {
type Error = crate::WaitForError;
fn try_from(s: &str) -> crate::Result<Self> {
s.parse()
}
}
impl TryFrom<String> for ValidatedDuration {
type Error = crate::WaitForError;
fn try_from(s: String) -> crate::Result<Self> {
s.as_str().parse()
}
}
impl fmt::Display for ValidatedDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let total_ms =
u64::try_from(self.0.as_millis().min(u128::from(u64::MAX))).unwrap_or(u64::MAX);
if total_ms >= 3_600_000 {
write!(f, "{hours}h", hours = total_ms / 3_600_000)
} else if total_ms >= 60_000 {
write!(f, "{minutes}m", minutes = total_ms / 60_000)
} else if total_ms >= 1_000 {
write!(f, "{seconds}s", seconds = total_ms / 1_000)
} else {
write!(f, "{total_ms}ms")
}
}
}
#[derive(Debug, Clone)]
pub struct WaitConfig {
pub timeout: Duration,
pub initial_interval: Duration,
pub max_interval: Duration,
pub wait_for_any: bool,
pub max_retries: Option<u32>,
pub connection_timeout: Duration,
pub cancellation_token: Option<CancellationToken>,
pub security_validator: Option<crate::security::SecurityValidator>,
pub rate_limiter: Option<crate::security::RateLimiter>,
}
impl Default for WaitConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
initial_interval: Duration::from_secs(1),
max_interval: Duration::from_secs(30),
wait_for_any: false,
max_retries: None,
connection_timeout: Duration::from_secs(10),
cancellation_token: None,
security_validator: None,
rate_limiter: None,
}
}
}
impl From<Duration> for WaitConfig {
fn from(timeout: Duration) -> Self {
Self {
timeout,
..Default::default()
}
}
}
impl PartialEq for WaitConfig {
fn eq(&self, other: &Self) -> bool {
self.timeout == other.timeout
&& self.initial_interval == other.initial_interval
&& self.max_interval == other.max_interval
&& self.wait_for_any == other.wait_for_any
&& self.max_retries == other.max_retries
&& self.connection_timeout == other.connection_timeout
}
}
impl Eq for WaitConfig {}
#[derive(Debug, Clone)]
pub struct WaitResult {
pub success: bool,
pub elapsed: Duration,
pub attempts: u32,
pub target_results: Vec<TargetResult>,
}
#[derive(Debug, Clone)]
pub struct TargetResult {
pub target: Target,
pub success: bool,
pub elapsed: Duration,
pub attempts: u32,
pub error: Option<String>,
}