use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
pub trait FormatChecker: Send + Sync {
fn check(&self, value: &str) -> bool;
fn format_name(&self) -> &str;
fn clone_box(&self) -> Box<dyn FormatChecker>;
}
impl Clone for Box<dyn FormatChecker> {
fn clone(&self) -> Self {
self.clone_box()
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
fn parse_date(s: &str) -> Option<(i32, u32, u32)> {
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 3 {
return None;
}
let year = parts[0].parse::<i32>().ok()?;
let month = parts[1].parse::<u32>().ok()?;
let day = parts[2].parse::<u32>().ok()?;
if parts[0].len() != 4 || parts[1].len() != 2 || parts[2].len() != 2 {
return None;
}
Some((year, month, day))
}
fn validate_date(s: &str) -> Option<()> {
let (year, month, day) = parse_date(s)?;
if !(1..=12).contains(&month) {
return None;
}
if day < 1 || day > days_in_month(year, month) {
return None;
}
Some(())
}
fn parse_time_val(s: &str) -> Option<()> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() < 2 || parts.len() > 3 {
return None;
}
let hour = parts[0].parse::<u32>().ok()?;
let minute = parts[1].parse::<u32>().ok()?;
if parts[0].len() != 2 || parts[1].len() != 2 {
return None;
}
if hour > 23 || minute > 59 {
return None;
}
if parts.len() == 3 {
let sec_parts: Vec<&str> = parts[2].split('.').collect();
let sec = sec_parts[0].parse::<u32>().ok()?;
if sec_parts[0].is_empty() || sec > 60 {
return None;
}
if sec_parts.len() == 2 {
if sec_parts[1].is_empty() {
return None;
}
if !sec_parts[1].chars().all(|c| c.is_ascii_digit()) {
return None;
}
}
}
Some(())
}
fn validate_tz(s: &str) -> Option<()> {
if s == "Z" || s == "z" {
return Some(());
}
if s.len() < 6 {
return None;
}
let sign = s.chars().next()?;
if sign != '+' && sign != '-' {
return None;
}
let rest = &s[1..];
let tz_parts: Vec<&str> = rest.split(':').collect();
if tz_parts.len() != 2 {
return None;
}
let h: u32 = tz_parts[0].parse().ok()?;
let m: u32 = tz_parts[1].parse().ok()?;
if h > 23 || m > 59 {
return None;
}
Some(())
}
pub struct DateTimeChecker;
impl FormatChecker for DateTimeChecker {
fn check(&self, value: &str) -> bool {
if let Some(t_pos) = value.find('T').or_else(|| value.find('t')) {
let date_part = &value[..t_pos];
let time_part = &value[t_pos + 1..];
if validate_date(date_part).is_none() {
return false;
}
let tz_start = time_part
.find('Z')
.or_else(|| time_part.find('z'))
.or_else(|| {
time_part.rfind('+')
})
.or_else(|| {
let bytes = time_part.as_bytes();
let mut pos = None;
for i in (0..bytes.len()).rev() {
if bytes[i] == b'-'
&& i > 0
&& time_part[..i].ends_with(|c: char| c.is_ascii_digit())
{
pos = Some(i);
break;
}
}
pos
});
match tz_start {
Some(pos) => {
let time_str = &time_part[..pos];
let tz_str = &time_part[pos..];
if parse_time_val(time_str).is_none() {
return false;
}
validate_tz(tz_str).is_some()
}
None => parse_time_val(time_part).is_some(),
}
} else {
false
}
}
fn format_name(&self) -> &'static str {
"date-time"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(DateTimeChecker)
}
}
pub struct DateChecker;
impl FormatChecker for DateChecker {
fn check(&self, value: &str) -> bool {
validate_date(value).is_some()
}
fn format_name(&self) -> &'static str {
"date"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(DateChecker)
}
}
pub struct TimeChecker;
impl FormatChecker for TimeChecker {
fn check(&self, value: &str) -> bool {
if let Some(pos) = value
.find('Z')
.or_else(|| value.find('z'))
.or_else(|| value.rfind('+'))
.or_else(|| {
let bytes = value.as_bytes();
let mut p = None;
for i in (0..bytes.len()).rev() {
if bytes[i] == b'-'
&& i > 0
&& value[..i].ends_with(|c: char| c.is_ascii_digit())
{
p = Some(i);
break;
}
}
p
})
{
let time_str = &value[..pos];
let tz_str = &value[pos..];
if parse_time_val(time_str).is_none() {
return false;
}
validate_tz(tz_str).is_some()
} else {
parse_time_val(value).is_some()
}
}
fn format_name(&self) -> &'static str {
"time"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(TimeChecker)
}
}
pub struct DurationChecker;
impl FormatChecker for DurationChecker {
fn check(&self, value: &str) -> bool {
let bytes = value.as_bytes();
if bytes.is_empty() || bytes[0] != b'P' {
return false;
}
let rest = &value[1..];
if rest.is_empty() {
return false;
}
let has_time = rest.contains('T');
if has_time {
let parts: Vec<&str> = rest.split('T').collect();
if parts.len() != 2 {
return false;
}
if parts[0].is_empty() && parts[1].is_empty() {
return false;
}
if !parse_duration_date(parts[0]) {
return false;
}
if !parse_duration_time(parts[1]) {
return false;
}
} else if !parse_duration_date(rest) {
return false;
}
true
}
fn format_name(&self) -> &'static str {
"duration"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(DurationChecker)
}
}
fn parse_duration_date(s: &str) -> bool {
if s.is_empty() {
return true;
}
let mut has = false;
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
let start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
if i == start || i >= chars.len() {
return false;
}
let designator = chars[i];
match designator {
'Y' | 'M' | 'W' | 'D' => {
has = true;
}
_ => return false,
}
i += 1;
}
has
}
fn parse_duration_time(s: &str) -> bool {
if s.is_empty() {
return true;
}
let mut has = false;
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
let start = i;
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
if i == start || i >= chars.len() {
return false;
}
let designator = chars[i];
match designator {
'H' | 'M' | 'S' => {
has = true;
}
_ => return false,
}
i += 1;
}
has
}
pub struct EmailChecker;
impl FormatChecker for EmailChecker {
fn check(&self, value: &str) -> bool {
let parts: Vec<&str> = value.split('@').collect();
if parts.len() != 2 {
return false;
}
let local = parts[0];
let domain = parts[1];
if local.is_empty() || domain.is_empty() {
return false;
}
if !domain.contains('.') {
return false;
}
let domain_parts: Vec<&str> = domain.split('.').collect();
domain_parts.iter().all(|d| !d.is_empty())
}
fn format_name(&self) -> &'static str {
"email"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(EmailChecker)
}
}
pub struct IdnEmailChecker;
impl FormatChecker for IdnEmailChecker {
fn check(&self, value: &str) -> bool {
let parts: Vec<&str> = value.split('@').collect();
if parts.len() != 2 {
return false;
}
!parts[0].is_empty() && !parts[1].is_empty()
}
fn format_name(&self) -> &'static str {
"idn-email"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(IdnEmailChecker)
}
}
pub struct HostnameChecker;
impl FormatChecker for HostnameChecker {
fn check(&self, value: &str) -> bool {
if value.is_empty() || value.len() > 253 {
return false;
}
let labels: Vec<&str> = value.split('.').collect();
labels.iter().all(|l| {
if l.is_empty() || l.len() > 63 {
return false;
}
let bytes = l.as_bytes();
if bytes[0] == b'-' || bytes[bytes.len() - 1] == b'-' {
return false;
}
l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
})
}
fn format_name(&self) -> &'static str {
"hostname"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(HostnameChecker)
}
}
pub struct IdnHostnameChecker;
impl FormatChecker for IdnHostnameChecker {
fn check(&self, value: &str) -> bool {
if value.is_empty() {
return false;
}
let labels: Vec<&str> = value.split('.').collect();
labels.iter().all(|l| !l.is_empty() && l.len() <= 63)
}
fn format_name(&self) -> &'static str {
"idn-hostname"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(IdnHostnameChecker)
}
}
pub struct Ipv4Checker;
impl FormatChecker for Ipv4Checker {
fn check(&self, value: &str) -> bool {
let parts: Vec<&str> = value.split('.').collect();
if parts.len() != 4 {
return false;
}
parts.iter().all(|p| {
if p.is_empty() || (p.len() > 1 && p.starts_with('0')) {
return false;
}
p.parse::<u8>().is_ok()
})
}
fn format_name(&self) -> &'static str {
"ipv4"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(Ipv4Checker)
}
}
pub struct Ipv6Checker;
impl FormatChecker for Ipv6Checker {
fn check(&self, value: &str) -> bool {
if value.is_empty() {
return false;
}
let has_compression = value.contains("::");
let segments: Vec<&str> = if has_compression {
let parts: Vec<&str> = value.split("::").collect();
if parts.len() != 2 {
return false;
}
let mut segs = Vec::new();
if !parts[0].is_empty() {
segs.extend(parts[0].split(':'));
}
if !parts[1].is_empty() {
segs.extend(parts[1].split(':'));
}
segs
} else {
value.split(':').collect()
};
let max_segs = if has_compression { 7 } else { 8 };
if segments.len() > max_segs {
return false;
}
segments.iter().all(|s| {
if s.is_empty() {
return false;
}
if s.len() > 4 {
return false;
}
u16::from_str_radix(s, 16).is_ok()
})
}
fn format_name(&self) -> &'static str {
"ipv6"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(Ipv6Checker)
}
}
pub struct UriChecker;
impl FormatChecker for UriChecker {
fn check(&self, value: &str) -> bool {
let Some(colon) = value.find(':') else {
return false;
};
let scheme = &value[..colon];
if scheme.is_empty() {
return false;
}
let mut chars = scheme.chars();
if !chars.next().is_some_and(|c| c.is_ascii_alphabetic()) {
return false;
}
for c in scheme.chars() {
if !c.is_ascii_alphanumeric() && c != '+' && c != '-' && c != '.' {
return false;
}
}
let after = &value[colon + 1..];
!after.is_empty()
}
fn format_name(&self) -> &'static str {
"uri"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(UriChecker)
}
}
pub struct UriReferenceChecker;
impl FormatChecker for UriReferenceChecker {
fn check(&self, value: &str) -> bool {
if value.is_empty() {
return true;
}
if value.contains(':') {
return UriChecker.check(value);
}
!value.chars().any(char::is_control)
}
fn format_name(&self) -> &'static str {
"uri-reference"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(UriReferenceChecker)
}
}
pub struct IriChecker;
impl FormatChecker for IriChecker {
fn check(&self, value: &str) -> bool {
let Some(colon) = value.find(':') else {
return false;
};
let scheme = &value[..colon];
if scheme.is_empty() {
return false;
}
let mut chars = scheme.chars();
if !chars.next().is_some_and(|c| c.is_ascii_alphabetic()) {
return false;
}
!value[colon + 1..].is_empty()
}
fn format_name(&self) -> &'static str {
"iri"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(IriChecker)
}
}
pub struct IriReferenceChecker;
impl FormatChecker for IriReferenceChecker {
fn check(&self, value: &str) -> bool {
if value.is_empty() {
return true;
}
if value.contains(':') {
return IriChecker.check(value);
}
!value.chars().any(char::is_control)
}
fn format_name(&self) -> &'static str {
"iri-reference"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(IriReferenceChecker)
}
}
pub struct UriTemplateChecker;
impl FormatChecker for UriTemplateChecker {
fn check(&self, value: &str) -> bool {
if value.is_empty() {
return false;
}
let mut depth: i32 = 0;
for c in value.chars() {
match c {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
if depth < 0 {
return false;
}
}
depth == 0
}
fn format_name(&self) -> &'static str {
"uri-template"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(UriTemplateChecker)
}
}
pub struct JsonPointerChecker;
impl FormatChecker for JsonPointerChecker {
fn check(&self, value: &str) -> bool {
if value.is_empty() {
return true;
}
if !value.starts_with('/') {
return false;
}
let mut chars = value.chars().peekable();
while let Some(c) = chars.next() {
if c == '~' {
match chars.peek() {
Some(&'0' | &'1') => {
chars.next();
}
_ => return false,
}
}
}
true
}
fn format_name(&self) -> &'static str {
"json-pointer"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(JsonPointerChecker)
}
}
pub struct RelativeJsonPointerChecker;
impl FormatChecker for RelativeJsonPointerChecker {
fn check(&self, value: &str) -> bool {
if value.is_empty() {
return false;
}
let mut i = 0;
let bytes = value.as_bytes();
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == 0 {
return false;
}
let rest = &value[i..];
if rest.is_empty() {
return false;
}
if rest == "#" {
return true;
}
JsonPointerChecker.check(rest)
}
fn format_name(&self) -> &'static str {
"relative-json-pointer"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(RelativeJsonPointerChecker)
}
}
pub struct RegexChecker;
impl FormatChecker for RegexChecker {
fn check(&self, value: &str) -> bool {
regex::Regex::new(value).is_ok()
}
fn format_name(&self) -> &'static str {
"regex"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(RegexChecker)
}
}
pub struct UuidChecker;
impl FormatChecker for UuidChecker {
fn check(&self, value: &str) -> bool {
let parts: Vec<&str> = value.split('-').collect();
if parts.len() != 5 {
return false;
}
let lens = [8, 4, 4, 4, 12];
parts
.iter()
.zip(lens)
.all(|(p, len)| p.len() == len && p.chars().all(|c| c.is_ascii_hexdigit()))
}
fn format_name(&self) -> &'static str {
"uuid"
}
fn clone_box(&self) -> Box<dyn FormatChecker> {
Box::new(UuidChecker)
}
}
#[must_use]
pub fn builtin_format(name: &str) -> Option<&'static dyn FormatChecker> {
match name {
"date-time" => Some(&DateTimeChecker),
"date" => Some(&DateChecker),
"time" => Some(&TimeChecker),
"duration" => Some(&DurationChecker),
"email" => Some(&EmailChecker),
"idn-email" => Some(&IdnEmailChecker),
"hostname" => Some(&HostnameChecker),
"idn-hostname" => Some(&IdnHostnameChecker),
"ipv4" => Some(&Ipv4Checker),
"ipv6" => Some(&Ipv6Checker),
"uri" => Some(&UriChecker),
"uri-reference" => Some(&UriReferenceChecker),
"iri" => Some(&IriChecker),
"iri-reference" => Some(&IriReferenceChecker),
"uri-template" => Some(&UriTemplateChecker),
"json-pointer" => Some(&JsonPointerChecker),
"relative-json-pointer" => Some(&RelativeJsonPointerChecker),
"regex" => Some(&RegexChecker),
"uuid" => Some(&UuidChecker),
_ => None,
}
}
#[must_use]
pub fn make_format_checker(
name: &str,
custom_formats: &alloc::collections::BTreeMap<String, Box<dyn FormatChecker>>,
) -> Option<Box<dyn FormatChecker>> {
if let Some(checker) = custom_formats.get(name) {
return clone_checker(checker.as_ref());
}
builtin_format(name).and_then(clone_checker)
}
pub fn clone_checker(checker: &dyn FormatChecker) -> Option<Box<dyn FormatChecker>> {
match checker.format_name() {
"date-time" => Some(Box::new(DateTimeChecker)),
"date" => Some(Box::new(DateChecker)),
"time" => Some(Box::new(TimeChecker)),
"duration" => Some(Box::new(DurationChecker)),
"email" => Some(Box::new(EmailChecker)),
"idn-email" => Some(Box::new(IdnEmailChecker)),
"hostname" => Some(Box::new(HostnameChecker)),
"idn-hostname" => Some(Box::new(IdnHostnameChecker)),
"ipv4" => Some(Box::new(Ipv4Checker)),
"ipv6" => Some(Box::new(Ipv6Checker)),
"uri" => Some(Box::new(UriChecker)),
"uri-reference" => Some(Box::new(UriReferenceChecker)),
"iri" => Some(Box::new(IriChecker)),
"iri-reference" => Some(Box::new(IriReferenceChecker)),
"uri-template" => Some(Box::new(UriTemplateChecker)),
"json-pointer" => Some(Box::new(JsonPointerChecker)),
"relative-json-pointer" => Some(Box::new(RelativeJsonPointerChecker)),
"regex" => Some(Box::new(RegexChecker)),
"uuid" => Some(Box::new(UuidChecker)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn date_time_valid() {
let c = DateTimeChecker;
assert!(c.check("2024-01-15T10:30:00Z"));
assert!(c.check("2024-01-15T10:30:00+05:30"));
assert!(c.check("2024-01-15T10:30:00.123Z"));
}
#[test]
fn date_time_invalid() {
let c = DateTimeChecker;
assert!(!c.check("2024-13-01T10:30:00Z")); assert!(!c.check("not-a-date"));
}
#[test]
fn date_valid() {
assert!(DateChecker.check("2024-01-15"));
assert!(DateChecker.check("2024-02-29")); }
#[test]
fn date_invalid() {
assert!(!DateChecker.check("2024-02-30")); assert!(!DateChecker.check("2023-02-29")); assert!(!DateChecker.check("not-a-date"));
}
#[test]
fn time_valid() {
assert!(TimeChecker.check("10:30:00Z"));
assert!(TimeChecker.check("10:30:00"));
}
#[test]
fn ipv4_valid() {
assert!(Ipv4Checker.check("192.168.0.1"));
assert!(Ipv4Checker.check("0.0.0.0"));
}
#[test]
fn ipv4_invalid() {
assert!(!Ipv4Checker.check("256.0.0.1"));
assert!(!Ipv4Checker.check("01.02.03.04")); }
#[test]
fn ipv6_valid() {
assert!(Ipv6Checker.check("::1"));
assert!(Ipv6Checker.check("2001:db8::1"));
assert!(Ipv6Checker.check("::"));
}
#[test]
fn email_valid() {
assert!(EmailChecker.check("user@example.com"));
}
#[test]
fn email_invalid() {
assert!(!EmailChecker.check("no-at-sign"));
assert!(!EmailChecker.check("@no-local.com"));
}
#[test]
fn hostname_valid() {
assert!(HostnameChecker.check("example.com"));
assert!(HostnameChecker.check("sub.domain.co.uk"));
}
#[test]
fn hostname_invalid() {
assert!(!HostnameChecker.check("-invalid.com"));
assert!(!HostnameChecker.check(""));
}
#[test]
fn uri_valid() {
assert!(UriChecker.check("https://example.com/path"));
assert!(UriChecker.check("ftp://files.example.com"));
}
#[test]
fn uri_invalid() {
assert!(!UriChecker.check("noscheme"));
}
#[test]
fn json_pointer_valid() {
assert!(JsonPointerChecker.check(""));
assert!(JsonPointerChecker.check("/foo/bar"));
assert!(JsonPointerChecker.check("/~0/~1")); }
#[test]
fn json_pointer_invalid() {
assert!(!JsonPointerChecker.check("no-slash"));
assert!(!JsonPointerChecker.check("/foo/~2")); }
#[test]
fn uuid_valid() {
assert!(UuidChecker.check("550e8400-e29b-41d4-a716-446655440000"));
}
#[test]
fn uuid_invalid() {
assert!(!UuidChecker.check("not-a-uuid"));
assert!(!UuidChecker.check("550e8400-e29b-41d4-a716"));
}
#[test]
fn regex_valid() {
assert!(RegexChecker.check(r"^\d+$"));
assert!(RegexChecker.check("[a-z]+"));
}
#[test]
fn regex_invalid() {
assert!(!RegexChecker.check(r"\w(")); }
#[test]
fn duration_valid() {
assert!(DurationChecker.check("P1Y2M3D"));
assert!(DurationChecker.check("PT1H30M"));
assert!(DurationChecker.check("P1DT1H"));
}
#[test]
fn duration_invalid() {
assert!(!DurationChecker.check("1Y")); assert!(!DurationChecker.check("P")); }
#[test]
fn uri_template_valid() {
assert!(UriTemplateChecker.check("/users/{id}"));
assert!(UriTemplateChecker.check("/search{?q}"));
}
#[test]
fn uri_template_invalid() {
assert!(!UriTemplateChecker.check("/unbalanced{"));
}
#[test]
fn relative_json_pointer_valid() {
assert!(RelativeJsonPointerChecker.check("0#"));
assert!(RelativeJsonPointerChecker.check("1/foo"));
}
}