use crate::Validator;
use crate::error::{Error, ErrorKind};
use tanzim_value::{Value, ValueType};
fn is_hostname(host: &str) -> bool {
if host.is_empty() || host.len() > 253 {
return false;
}
for label in host.split('.') {
let bytes = label.as_bytes();
if bytes.is_empty() || bytes.len() > 63 {
return false;
}
if bytes[0] == b'-' || bytes[bytes.len() - 1] == b'-' {
return false;
}
for &byte in bytes {
if !byte.is_ascii_alphanumeric() && byte != b'-' {
return false;
}
}
}
true
}
fn as_string(value: &mut Value) -> Result<&mut String, Error> {
match value {
Value::String(text) => Ok(text),
other => Err(Error::new(ErrorKind::Type {
expected: ValueType::String,
found: other.type_name(),
})),
}
}
#[derive(Debug, Clone, Default)]
pub struct Host;
impl Host {
pub fn new() -> Self {
Self
}
}
impl Validator for Host {
fn validate(&self, value: &mut Value) -> Result<(), Error> {
let text = as_string(value)?;
if text.parse::<std::net::IpAddr>().is_ok() || is_hostname(text) {
Ok(())
} else {
Err(Error::new(ErrorKind::Format { expected: "host" }))
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Domain {
require_dot: bool,
}
impl Domain {
pub fn new() -> Self {
Self::default()
}
pub fn require_dot(mut self) -> Self {
self.require_dot = true;
self
}
}
impl Validator for Domain {
fn validate(&self, value: &mut Value) -> Result<(), Error> {
let text = as_string(value)?;
*text = text.to_lowercase();
if !is_hostname(text) || (self.require_dot && !text.contains('.')) {
return Err(Error::new(ErrorKind::Format { expected: "domain" }));
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct Email;
impl Email {
pub fn new() -> Self {
Self
}
}
impl Validator for Email {
fn validate(&self, value: &mut Value) -> Result<(), Error> {
let text = as_string(value)?;
let (local, domain) = match text.rsplit_once('@') {
Some(parts) => parts,
None => return Err(Error::new(ErrorKind::Format { expected: "email" })),
};
if local.is_empty() || local.len() > 64 || !is_hostname(domain) || !domain.contains('.') {
return Err(Error::new(ErrorKind::Format { expected: "email" }));
}
*text = format!("{local}@{}", domain.to_lowercase());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Port {
allow_zero: bool,
privileged_ok: bool,
}
impl Default for Port {
fn default() -> Self {
Self {
allow_zero: false,
privileged_ok: true,
}
}
}
impl Port {
pub fn new() -> Self {
Self::default()
}
pub fn allow_zero(mut self) -> Self {
self.allow_zero = true;
self
}
pub fn privileged_ok(mut self, allowed: bool) -> Self {
self.privileged_ok = allowed;
self
}
}
impl Validator for Port {
fn validate(&self, value: &mut Value) -> Result<(), Error> {
let min = if self.allow_zero { 0 } else { 1 };
crate::Integer::new().range(min, 65535).validate(value)?;
let port = match value.as_int() {
Some(port) => port,
None => unreachable!("Integer validation produced a non-integer"),
};
if !self.privileged_ok && (1..1024).contains(&port) {
return Err(Error::new(ErrorKind::Format {
expected: "non-privileged port (>= 1024)",
}));
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct IpAddr {
v4_only: bool,
v6_only: bool,
}
impl IpAddr {
pub fn new() -> Self {
Self::default()
}
pub fn v4_only(mut self) -> Self {
self.v4_only = true;
self.v6_only = false;
self
}
pub fn v6_only(mut self) -> Self {
self.v6_only = true;
self.v4_only = false;
self
}
}
impl Validator for IpAddr {
fn validate(&self, value: &mut Value) -> Result<(), Error> {
let text = as_string(value)?;
let parsed = match text.parse::<std::net::IpAddr>() {
Ok(parsed) => parsed,
Err(_) => {
return Err(Error::new(ErrorKind::Format {
expected: "ip address",
}));
}
};
if self.v4_only && !parsed.is_ipv4() {
return Err(Error::new(ErrorKind::Format {
expected: "IPv4 address",
}));
}
if self.v6_only && !parsed.is_ipv6() {
return Err(Error::new(ErrorKind::Format {
expected: "IPv6 address",
}));
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct SocketAddr;
impl SocketAddr {
pub fn new() -> Self {
Self
}
}
impl Validator for SocketAddr {
fn validate(&self, value: &mut Value) -> Result<(), Error> {
let text = as_string(value)?;
if text.parse::<std::net::SocketAddr>().is_ok() {
return Ok(());
}
if let Some((host, port)) = text.rsplit_once(':') {
let port_ok = match port.parse::<u16>() {
Ok(number) => number != 0,
Err(_) => false,
};
if port_ok && is_hostname(host) {
return Ok(());
}
}
Err(Error::new(ErrorKind::Format {
expected: "socket address",
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn string(text: &str) -> Value {
Value::String(text.to_string())
}
#[test]
fn host_accepts_name_and_ip() {
assert!(Host::new().validate(&mut string("example.com")).is_ok());
assert!(Host::new().validate(&mut string("127.0.0.1")).is_ok());
assert!(Host::new().validate(&mut string("bad_host!")).is_err());
}
#[test]
fn domain_lowercases_and_requires_dot() {
let mut value = string("Example.COM");
Domain::new().require_dot().validate(&mut value).unwrap();
assert_eq!(value, string("example.com"));
assert!(
Domain::new()
.require_dot()
.validate(&mut string("localhost"))
.is_err()
);
}
#[test]
fn email_validates_and_lowercases_domain() {
let mut value = string("User@Example.COM");
Email::new().validate(&mut value).unwrap();
assert_eq!(value, string("User@example.com"));
assert!(Email::new().validate(&mut string("nope")).is_err());
}
#[test]
fn port_range_and_privileged() {
let mut value = string("8080");
Port::new().validate(&mut value).unwrap();
assert_eq!(value, Value::Int(8080));
assert!(Port::new().validate(&mut Value::Int(0)).is_err());
assert!(
Port::new()
.allow_zero()
.validate(&mut Value::Int(0))
.is_ok()
);
assert!(
Port::new()
.privileged_ok(false)
.validate(&mut Value::Int(80))
.is_err()
);
}
#[test]
fn ip_addr_family_filter() {
assert!(
IpAddr::new()
.v4_only()
.validate(&mut string("10.0.0.1"))
.is_ok()
);
assert!(
IpAddr::new()
.v4_only()
.validate(&mut string("::1"))
.is_err()
);
assert!(IpAddr::new().v6_only().validate(&mut string("::1")).is_ok());
}
#[test]
fn socket_addr_forms() {
assert!(
SocketAddr::new()
.validate(&mut string("127.0.0.1:8080"))
.is_ok()
);
assert!(
SocketAddr::new()
.validate(&mut string("example.com:443"))
.is_ok()
);
assert!(
SocketAddr::new()
.validate(&mut string("example.com"))
.is_err()
);
}
}