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, PartialOrd, Ord)]
pub struct Port(NonZeroU16);
impl Port {
#[must_use]
pub const fn new(port: u16) -> Option<Self> {
match NonZeroU16::new(port) {
Some(nz) => Some(Self(nz)),
None => None,
}
}
#[must_use]
pub const fn well_known(port: u16) -> Option<Self> {
if port == 0 || port > 1023 {
None
} else {
Self::new(port)
}
}
#[must_use]
pub const fn registered(port: u16) -> Option<Self> {
if port < 1024 || port > 49151 {
None
} else {
Self::new(port)
}
}
#[must_use]
pub const fn dynamic(port: u16) -> Option<Self> {
if port < 49152 {
None
} else {
Self::new(port)
}
}
#[must_use]
pub const fn new_unchecked(port: u16) -> Self {
if let Some(nz) = NonZeroU16::new(port) {
Self(nz)
} else {
let safe_port = if port == 0 { 1 } else { port };
Self(unsafe { NonZeroU16::new_unchecked(safe_port) })
}
}
#[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]
pub const fn get(&self) -> u16 {
self.0.get()
}
}
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 {
fn from(port: Port) -> Self {
port.0.get()
}
}
impl From<Port> for NonZeroU16 {
fn from(port: Port) -> Self {
port.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]
pub fn as_str(&self) -> &str {
&self.0
}
#[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 {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for Hostname {
fn borrow(&self) -> &str {
&self.0
}
}
#[derive(Error, Debug)]
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)]
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)]
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>,
}