use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
sync::Arc,
};
use email_address::{EmailAddress, Options as EmailAddressOptions};
use serde_json::{Map, Value};
use unicode_general_category::{get_general_category, GeneralCategory};
use uuid_simd::{parse_hyphenated, Out};
use crate::{
compiler,
error::{no_error, ValidationError},
evaluation::Annotations,
keywords::CompilationResult,
paths::{LazyLocation, Location, RefTracker},
types::JsonType,
validator::{EvaluationResult, Validate, ValidationContext},
Draft,
};
fn is_valid_uri_template(template: &str) -> bool {
let bytes = template.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
if bytes[i] == b'{' {
i += 1;
if i >= len {
return false; }
if is_operator(bytes[i]) {
i += 1;
if i >= len {
return false;
}
}
if !parse_varspec(bytes, &mut i) {
return false;
}
while i < len && bytes[i] == b',' {
i += 1;
if !parse_varspec(bytes, &mut i) {
return false;
}
}
if i >= len || bytes[i] != b'}' {
return false;
}
i += 1;
} else if bytes[i] == b'}' {
return false;
} else {
if !parse_literal(bytes, &mut i) {
return false;
}
}
}
true
}
#[inline]
fn is_operator(b: u8) -> bool {
matches!(
b,
b'+' | b'#' | b'.' | b'/' | b';' | b'?' | b'&' | b'=' | b',' | b'!' | b'@' | b'|'
)
}
#[inline]
fn parse_varspec(bytes: &[u8], i: &mut usize) -> bool {
let len = bytes.len();
if !parse_varname(bytes, i) {
return false;
}
if *i < len {
match bytes[*i] {
b':' => {
*i += 1;
if *i >= len {
return false;
}
if !bytes[*i].is_ascii_digit() || bytes[*i] == b'0' {
return false;
}
*i += 1;
let mut digit_count = 1;
while *i < len && bytes[*i].is_ascii_digit() && digit_count < 4 {
*i += 1;
digit_count += 1;
}
}
b'*' => {
*i += 1;
}
_ => {}
}
}
true
}
#[inline]
fn parse_varname(bytes: &[u8], i: &mut usize) -> bool {
let len = bytes.len();
if !parse_varchar(bytes, i) {
return false;
}
while *i < len {
if bytes[*i] == b'.' {
*i += 1;
if !parse_varchar(bytes, i) {
return false;
}
} else if is_varchar_start(bytes[*i]) || bytes[*i] == b'%' {
if !parse_varchar(bytes, i) {
return false;
}
} else {
break;
}
}
true
}
#[inline]
fn parse_varchar(bytes: &[u8], i: &mut usize) -> bool {
let len = bytes.len();
let start = *i;
while *i < len {
if is_varchar_start(bytes[*i]) {
*i += 1;
} else if bytes[*i] == b'%' {
if *i + 2 >= len {
return false;
}
if !is_hex_digit(bytes[*i + 1]) || !is_hex_digit(bytes[*i + 2]) {
return false;
}
*i += 3;
} else {
break;
}
}
*i > start
}
#[inline]
fn is_varchar_start(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
#[inline]
fn is_hex_digit(b: u8) -> bool {
b.is_ascii_hexdigit()
}
#[inline]
fn parse_literal(bytes: &[u8], i: &mut usize) -> bool {
let len = bytes.len();
let b = bytes[*i];
if b == b'%' {
if *i + 2 >= len {
return false;
}
if !is_hex_digit(bytes[*i + 1]) || !is_hex_digit(bytes[*i + 2]) {
return false;
}
*i += 3;
true
} else if is_literal_char(b) {
*i += 1;
true
} else {
false
}
}
#[inline]
fn is_literal_char(b: u8) -> bool {
!matches!(
b,
0x00..=0x20
| b'"'
| b'\''
| b'<'
| b'>'
| b'%'
| b'\\'
| b'^'
| b'`'
| b'{'
| b'|'
| b'}'
| 0x7F
)
}
fn is_valid_json_pointer(pointer: &str) -> bool {
if pointer.is_empty() {
return true;
}
let mut chars = pointer.chars();
if chars.next() != Some('/') {
return false;
}
is_valid_json_pointer_impl(chars)
}
fn is_valid_relative_json_pointer(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some('0') => {
match chars.next() {
Some('#') => chars.next().is_none(),
Some('/') => is_valid_json_pointer_impl(chars),
None => true,
_ => false,
}
}
Some(c) if c.is_ascii_digit() => {
while let Some(c) = chars.next() {
match c {
'#' => return chars.next().is_none(),
'/' => return is_valid_json_pointer_impl(chars),
c if c.is_ascii_digit() => {}
_ => return false,
}
}
true
}
_ => false,
}
}
#[inline]
fn is_valid_json_pointer_impl<I: Iterator<Item = char>>(chars: I) -> bool {
let mut escaped = false;
for c in chars {
match c {
'/' if !escaped => escaped = false,
'~' if !escaped => escaped = true,
'0' | '1' if escaped => escaped = false,
'\x00'..='\x2E' | '\x30'..='\x7D' | '\x7F'..='\u{10FFFF}' if !escaped => {}
_ => return false,
}
}
!escaped
}
fn is_valid_date(date: &str) -> bool {
if date.len() != 10 {
return false;
}
let bytes = date.as_bytes();
if bytes[4] != b'-' || bytes[7] != b'-' {
return false;
}
let Some(year) = parse_four_digits(&bytes[0..4]) else {
return false;
};
let Some(month) = parse_two_digits(&bytes[5..7]) else {
return false;
};
if !(1..=12).contains(&month) {
return false;
}
let Some(day) = parse_two_digits(&bytes[8..10]) else {
return false;
};
if day == 0 {
return false;
}
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => day <= 31,
4 | 6 | 9 | 11 => day <= 30,
2 => {
if is_leap_year(year) {
day <= 29
} else {
day <= 28
}
}
_ => unreachable!("Month value is checked above"),
}
}
#[inline]
fn is_leap_year(year: u16) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
#[inline]
fn parse_four_digits(bytes: &[u8]) -> Option<u16> {
let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let sub = value.wrapping_sub(0x3030_3030);
if sub.wrapping_add(0x0606_0606) & 0xF0F0_F0F0 != 0 {
return None;
}
let val = (sub & 0x0F0F_0F0F).wrapping_mul(2561) >> 8;
Some(((val & 0x00FF_00FF).wrapping_mul(6_553_601) >> 16) as u16)
}
#[inline]
fn parse_two_digits(bytes: &[u8]) -> Option<u8> {
let value = u16::from_le_bytes([bytes[0], bytes[1]]);
let sub = value.wrapping_sub(0x3030);
if sub.wrapping_add(0x0606) & 0xF0F0 != 0 {
return None;
}
Some(((sub & 0x0F0F).wrapping_mul(2561) >> 8) as u8)
}
macro_rules! handle_offset {
($sign:tt, $i:ident, $bytes:expr, $hour:expr, $minute:expr, $second:expr) => {{
if $bytes.len() - $i != 6 {
return false;
}
$i += 1;
if $bytes[$i + 2] != b':' {
return false;
}
let Some(offset_hh) = parse_two_digits(&$bytes[$i..$i + 2]) else {
return false;
};
let Some(offset_mm) = parse_two_digits(&$bytes[$i + 3..$i + 5]) else {
return false;
};
if offset_hh > 23 || offset_mm > 59 {
return false;
}
if $second == 60 {
let mut utc_hh = i16::from($hour);
let mut utc_mm = i16::from($minute);
let offset_hh = i16::from(offset_hh);
let offset_mm = i16::from(offset_mm);
utc_hh $sign offset_hh;
utc_mm $sign offset_mm;
utc_hh += utc_mm / 60;
utc_mm %= 60;
if utc_mm < 0 {
utc_mm += 60;
utc_hh -= 1;
}
utc_hh = (utc_hh + 24) % 24;
utc_hh == 23 && utc_mm == 59
} else {
true
}
}};
}
fn is_valid_time(time: &str) -> bool {
let bytes = time.as_bytes();
let len = bytes.len();
if len < 9 {
return false;
}
if bytes[2] != b':' || bytes[5] != b':' {
return false;
}
let Some(hour) = parse_two_digits(&bytes[..2]) else {
return false;
};
let Some(minute) = parse_two_digits(&bytes[3..5]) else {
return false;
};
let Some(second) = parse_two_digits(&bytes[6..8]) else {
return false;
};
if hour > 23 || minute > 59 || second > 60 {
return false;
}
let mut i = 8;
if i < len && bytes[i] == b'.' {
i += 1;
let mut has_digit = false;
while i < len && bytes[i].is_ascii_digit() {
has_digit = true;
i += 1;
}
if !has_digit {
return false;
}
}
if i == len {
return false;
}
match bytes[i] {
b'Z' | b'z' => i == len - 1 && (second != 60 || (hour == 23 && minute == 59)),
b'+' => handle_offset!(-=, i, bytes, hour, minute, second),
b'-' => handle_offset!(+=, i, bytes, hour, minute, second),
_ => false,
}
}
fn is_valid_datetime(datetime: &str) -> bool {
let Some(t_pos) = datetime.bytes().position(|b| b == b'T' || b == b't') else {
return false;
};
let (date_part, time_part) = datetime.split_at(t_pos);
is_valid_date(date_part) && is_valid_time(&time_part[1..])
}
fn is_valid_email_impl<F>(
email: &str,
is_valid_hostname_impl: F,
options: Option<&EmailAddressOptions>,
) -> bool
where
F: Fn(&str) -> bool,
{
let parsed = if let Some(opts) = options {
EmailAddress::parse_with_options(email, *opts)
} else {
EmailAddress::from_str(email)
};
if let Ok(parsed) = parsed {
let domain = parsed.domain();
if let Some(domain) = domain.strip_prefix('[').and_then(|d| d.strip_suffix(']')) {
if let Some(domain) = domain.strip_prefix("IPv6:") {
domain.parse::<Ipv6Addr>().is_ok()
} else {
domain.parse::<Ipv4Addr>().is_ok()
}
} else {
is_valid_hostname_impl(domain)
}
} else {
false
}
}
fn is_valid_email(email: &str, options: Option<&EmailAddressOptions>) -> bool {
is_valid_email_impl(email, is_valid_hostname, options)
}
fn is_valid_idn_email(email: &str, options: Option<&EmailAddressOptions>) -> bool {
is_valid_email_impl(email, is_valid_idn_hostname, options)
}
const VALID_HOSTNAME_CHARS: [bool; 256] = {
let mut table = [false; 256];
let mut byte: u8 = 0;
while byte < 255 {
table[byte as usize] = matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-');
byte += 1;
}
table[255] = matches!(255u8, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-');
table
};
#[inline]
fn is_punycode_label(label: &[u8]) -> bool {
label.len() >= 4 && label[0] == b'x' && label[1] == b'n' && label[2] == b'-' && label[3] == b'-'
}
#[inline]
fn validate_hostname_label(label: &[u8]) -> bool {
!label.is_empty() && label.len() <= 63 && label[0] != b'-' && *label.last().unwrap() != b'-'
}
fn is_valid_ascii_hostname(hostname: &str) -> bool {
let hostname_bytes = hostname.as_bytes();
let len = hostname_bytes.len();
if len == 0 || len > 253 || hostname_bytes[len - 1] == b'.' {
return false;
}
let mut label_start = 0;
let mut i = 0;
while i < len {
if hostname_bytes[i] == b'.' {
if !validate_hostname_label(&hostname_bytes[label_start..i]) {
return false;
}
label_start = i + 1;
} else if !VALID_HOSTNAME_CHARS[hostname_bytes[i] as usize] {
return false;
}
i += 1;
}
validate_hostname_label(&hostname_bytes[label_start..])
}
fn is_valid_hostname_rfc1034(hostname: &str) -> bool {
is_valid_ascii_hostname(hostname)
}
fn is_valid_hostname(hostname: &str) -> bool {
if !is_valid_ascii_hostname(hostname) {
return false;
}
for label in hostname.as_bytes().split(|&b| b == b'.') {
if label.len() >= 4 && label[2] == b'-' && label[3] == b'-' && !is_punycode_label(label) {
return false;
}
if is_punycode_label(label) {
let payload = std::str::from_utf8(&label[4..]).expect("ASCII label already validated");
let Some(decoded) = idna::punycode::decode_to_string(payload) else {
return false;
};
if !validate_unicode_label(&decoded) {
return false;
}
}
}
true
}
fn validate_unicode_label(label: &str) -> bool {
let mut chars = label.chars().peekable();
if let Some(&first) = chars.peek() {
let category = get_general_category(first);
if matches!(
category,
GeneralCategory::SpacingMark
| GeneralCategory::NonspacingMark
| GeneralCategory::EnclosingMark
) {
return false;
}
}
let mut previous = None;
let mut has_katakana_middle_dot = false;
let mut has_hiragana_katakana_han = false;
let mut has_arabic_indic_digits = false;
let mut has_extended_arabic_indic_digits = false;
while let Some(current) = chars.next() {
match current {
'\u{200D}'
if !previous.is_some_and(|prev| {
matches!(
prev,
'\u{094D}'
| '\u{09CD}'
| '\u{0A4D}'
| '\u{0ACD}'
| '\u{0B4D}'
| '\u{0BCD}'
| '\u{0C4D}'
| '\u{0CCD}'
| '\u{0D4D}'
| '\u{0DCA}'
| '\u{0E3A}'
| '\u{0F84}'
| '\u{1039}'
| '\u{1714}'
| '\u{1734}'
| '\u{17D2}'
| '\u{1A60}'
| '\u{1B44}'
| '\u{1BAA}'
| '\u{1BF2}'
| '\u{1BF3}'
| '\u{2D7F}'
| '\u{A806}'
| '\u{A8C4}'
| '\u{A953}'
| '\u{ABED}'
| '\u{10A3F}'
| '\u{11046}'
| '\u{1107F}'
| '\u{110B9}'
| '\u{11133}'
| '\u{111C0}'
| '\u{11235}'
| '\u{112EA}'
| '\u{1134D}'
| '\u{11442}'
| '\u{114C2}'
| '\u{115BF}'
| '\u{1163F}'
| '\u{116B6}'
| '\u{1172B}'
| '\u{11839}'
| '\u{119E0}'
| '\u{11A34}'
| '\u{11A47}'
| '\u{11A99}'
| '\u{11C3F}'
| '\u{11D44}'
| '\u{11D45}'
| '\u{11D97}'
)
}) =>
{
return false;
}
'\u{00B7}' if previous != Some('l') || chars.peek() != Some(&'l') => return false,
'\u{0375}'
if !chars
.peek()
.is_some_and(|next| ('\u{0370}'..='\u{03FF}').contains(next)) =>
{
return false
}
'\u{05F3}' | '\u{05F4}'
if !previous.is_some_and(|prev| ('\u{0590}'..='\u{05FF}').contains(&prev)) =>
{
return false
}
'\u{30FB}' => has_katakana_middle_dot = true,
'\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}' | '\u{4E00}'..='\u{9FFF}' => {
has_hiragana_katakana_han = true;
}
'\u{0660}'..='\u{0669}' => has_arabic_indic_digits = true,
'\u{06F0}'..='\u{06F9}' => has_extended_arabic_indic_digits = true,
'\u{0640}' | '\u{07FA}' | '\u{302E}' | '\u{302F}' | '\u{3031}' | '\u{3032}'
| '\u{3033}' | '\u{3034}' | '\u{3035}' | '\u{303B}' => return false,
_ => {}
}
previous = Some(current);
}
if (has_katakana_middle_dot && !has_hiragana_katakana_han)
|| (has_arabic_indic_digits && has_extended_arabic_indic_digits)
{
return false;
}
true
}
fn is_valid_idn_hostname(hostname: &str) -> bool {
use idna::uts46::{AsciiDenyList, DnsLength, Hyphens, Uts46};
let Ok(ascii_hostname) = Uts46::new().to_ascii(
hostname.as_bytes(),
AsciiDenyList::STD3,
Hyphens::Check,
DnsLength::Verify,
) else {
return false;
};
if !is_valid_hostname(&ascii_hostname) {
return false;
}
let (unicode_hostname, _) = Uts46::new().to_unicode(
ascii_hostname.as_bytes(),
AsciiDenyList::EMPTY,
Hyphens::Allow,
);
unicode_hostname
.split('.')
.all(|label| !label.is_empty() && validate_unicode_label(label))
}
#[inline]
fn unit_index(units: &[u8], unit: u8) -> Option<usize> {
units.iter().position(|&u| u == unit)
}
fn is_valid_duration(duration: &str) -> bool {
let bytes = duration.as_bytes();
let len = bytes.len();
if len < 2 || bytes[0] != b'P' {
return false;
}
let mut i = 1;
let mut has_component = false;
let mut has_time = false;
let mut last_date_unit = 0;
let mut last_time_unit = 0;
let mut has_weeks = false;
let mut has_time_component = false;
let mut seen_units = 0u8;
let date_units = [b'Y', b'M', b'W', b'D'];
let time_units = [b'H', b'M', b'S'];
while i < len {
if bytes[i] == b'T' {
if has_time {
return false;
}
has_time = true;
i += 1;
continue;
}
let start = i;
while i < len && bytes[i].is_ascii_digit() {
i += 1;
}
if i == start || i == len {
return false;
}
let unit = bytes[i];
if !has_time {
if let Some(idx) = unit_index(&date_units, unit) {
if unit == b'W' {
if has_component {
return false;
}
has_weeks = true;
} else if has_weeks {
return false;
}
if idx < last_date_unit || (seen_units & (1 << idx) != 0) {
return false;
}
seen_units |= 1 << idx;
last_date_unit = idx;
} else {
return false;
}
} else if let Some(idx) = unit_index(&time_units, unit) {
if idx < last_time_unit || (seen_units & (1 << (idx + 4)) != 0) {
return false;
}
seen_units |= 1 << (idx + 4);
last_time_unit = idx;
has_time_component = true;
} else {
return false;
}
has_component = true;
i += 1;
}
if !has_component || (has_time && !has_time_component) {
return false;
}
let has_date_y = seen_units & (1 << 0) != 0; let has_date_m = seen_units & (1 << 1) != 0; let has_date_d = seen_units & (1 << 3) != 0; if has_date_y && has_date_d && !has_date_m {
return false;
}
let has_time_h = seen_units & (1 << 4) != 0; let has_time_m = seen_units & (1 << 5) != 0; let has_time_s = seen_units & (1 << 6) != 0; if has_time_h && has_time_s && !has_time_m {
return false;
}
true
}
fn is_valid_ipv4(ip: &str) -> bool {
Ipv4Addr::from_str(ip).is_ok()
}
fn is_valid_ipv6(ip: &str) -> bool {
Ipv6Addr::from_str(ip).is_ok()
}
fn is_valid_iri(iri: &str) -> bool {
referencing::Iri::parse(iri).is_ok()
}
fn is_valid_iri_reference(iri_reference: &str) -> bool {
referencing::IriRef::parse(iri_reference).is_ok()
}
fn is_valid_uri(uri: &str) -> bool {
referencing::Uri::parse(uri).is_ok()
}
fn is_valid_uri_reference(uri_reference: &str) -> bool {
referencing::UriRef::parse(uri_reference).is_ok()
}
fn is_valid_uuid(uuid: &str) -> bool {
let mut out = [0; 16];
parse_hyphenated(uuid.as_bytes(), Out::from_mut(&mut out)).is_ok()
}
macro_rules! impl_format_evaluate {
() => {
fn evaluate(
&self,
instance: &Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> EvaluationResult {
if !instance.is_string() {
return EvaluationResult::valid_empty();
}
let errors: Vec<_> = self
.iter_errors(instance, location, tracker, ctx)
.map(|e| crate::evaluation::ErrorDescription::from_validation_error(&e))
.collect();
let mut result = if errors.is_empty() {
EvaluationResult::valid_empty()
} else {
EvaluationResult::invalid_empty(errors)
};
result.annotate(Annotations::from_arc(Arc::clone(&self.annotation)));
result
}
};
}
macro_rules! format_validators {
($(($validator:ident, $format:expr, $validation_fn:ident)),+ $(,)?) => {
$(
struct $validator {
location: Location,
annotation: Arc<Value>,
}
impl $validator {
pub(crate) fn compile<'a>(ctx: &compiler::Context) -> CompilationResult<'a> {
let location = ctx.location().join("format");
let annotation = Arc::new(Value::String($format.to_owned()));
Ok(Box::new($validator { location, annotation }))
}
}
impl Validate for $validator {
fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool {
if let Value::String(item) = instance {
$validation_fn(item)
} else {
true
}
}
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if let Value::String(_item) = instance {
if !self.is_valid(instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance,
$format,
));
}
}
Ok(())
}
impl_format_evaluate!();
}
)+
};
}
format_validators!(
(DateValidator, "date", is_valid_date),
(DateTimeValidator, "date-time", is_valid_datetime),
(DurationValidator, "duration", is_valid_duration),
(
HostnameValidatorDraft4,
"hostname",
is_valid_hostname_rfc1034
),
(HostnameValidator, "hostname", is_valid_hostname),
(IdnHostnameValidator, "idn-hostname", is_valid_idn_hostname),
(IpV4Validator, "ipv4", is_valid_ipv4),
(IpV6Validator, "ipv6", is_valid_ipv6),
(IriValidator, "iri", is_valid_iri),
(
IriReferenceValidator,
"iri-reference",
is_valid_iri_reference
),
(JsonPointerValidator, "json-pointer", is_valid_json_pointer),
(
RelativeJsonPointerValidator,
"relative-json-pointer",
is_valid_relative_json_pointer
),
(TimeValidator, "time", is_valid_time),
(UriValidator, "uri", is_valid_uri),
(
UriReferenceValidator,
"uri-reference",
is_valid_uri_reference
),
(UriTemplateValidator, "uri-template", is_valid_uri_template),
(UuidValidator, "uuid", is_valid_uuid),
);
struct RegexValidator {
location: Location,
annotation: Arc<Value>,
}
impl RegexValidator {
pub(crate) fn compile<'a>(ctx: &compiler::Context) -> CompilationResult<'a> {
let location = ctx.location().join("format");
let annotation = Arc::new(Value::String("regex".to_owned()));
Ok(Box::new(RegexValidator {
location,
annotation,
}))
}
}
impl Validate for RegexValidator {
fn is_valid(&self, instance: &Value, ctx: &mut ValidationContext) -> bool {
if let Value::String(item) = instance {
ctx.is_valid_ecma_regex(item)
} else {
true
}
}
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if let Value::String(_) = instance {
if !self.is_valid(instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance,
"regex",
));
}
}
Ok(())
}
impl_format_evaluate!();
}
struct EmailValidator {
location: Location,
annotation: Arc<Value>,
email_options: Option<EmailAddressOptions>,
}
impl EmailValidator {
pub(crate) fn compile<'a>(ctx: &compiler::Context) -> CompilationResult<'a> {
let location = ctx.location().join("format");
let annotation = Arc::new(Value::String("email".to_owned()));
let email_options = ctx.config().email_options().copied();
Ok(Box::new(EmailValidator {
location,
annotation,
email_options,
}))
}
}
impl Validate for EmailValidator {
fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool {
if let Value::String(item) = instance {
is_valid_email(item, self.email_options.as_ref())
} else {
true
}
}
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if let Value::String(_item) = instance {
if !self.is_valid(instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance,
"email",
));
}
}
Ok(())
}
impl_format_evaluate!();
}
struct IdnEmailValidator {
location: Location,
annotation: Arc<Value>,
email_options: Option<EmailAddressOptions>,
}
impl IdnEmailValidator {
pub(crate) fn compile<'a>(ctx: &compiler::Context) -> CompilationResult<'a> {
let location = ctx.location().join("format");
let annotation = Arc::new(Value::String("idn-email".to_owned()));
let email_options = ctx.config().email_options().copied();
Ok(Box::new(IdnEmailValidator {
location,
annotation,
email_options,
}))
}
}
impl Validate for IdnEmailValidator {
fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool {
if let Value::String(item) = instance {
is_valid_idn_email(item, self.email_options.as_ref())
} else {
true
}
}
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if let Value::String(_item) = instance {
if !self.is_valid(instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance,
"idn-email",
));
}
}
Ok(())
}
impl_format_evaluate!();
}
struct CustomFormatValidator {
location: Location,
annotation: Arc<Value>,
format_name: String,
check: Arc<dyn Format>,
}
impl CustomFormatValidator {
pub(crate) fn compile<'a>(
ctx: &compiler::Context,
format_name: String,
check: Arc<dyn Format>,
) -> CompilationResult<'a> {
let location = ctx.location().join("format");
let annotation = Arc::new(Value::String(format_name.clone()));
Ok(Box::new(CustomFormatValidator {
location,
annotation,
format_name,
check,
}))
}
}
impl Validate for CustomFormatValidator {
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if self.is_valid(instance, ctx) {
Ok(())
} else {
Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance,
self.format_name.clone(),
))
}
}
fn is_valid(&self, instance: &Value, _ctx: &mut ValidationContext) -> bool {
if let Value::String(item) = instance {
self.check.is_valid(item)
} else {
true
}
}
impl_format_evaluate!();
}
struct AnnotationOnlyFormatValidator {
annotation: Arc<Value>,
}
impl Validate for AnnotationOnlyFormatValidator {
fn is_valid(&self, _instance: &Value, _ctx: &mut ValidationContext) -> bool {
true
}
fn validate<'i>(
&self,
_instance: &'i Value,
_location: &LazyLocation,
_tracker: Option<&RefTracker>,
_ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
Ok(())
}
fn iter_errors<'i>(
&self,
_instance: &'i Value,
_location: &LazyLocation,
_tracker: Option<&RefTracker>,
_ctx: &mut ValidationContext,
) -> crate::error::ErrorIterator<'i> {
no_error()
}
fn evaluate(
&self,
instance: &Value,
_location: &LazyLocation,
_tracker: Option<&RefTracker>,
_ctx: &mut ValidationContext,
) -> EvaluationResult {
if !instance.is_string() {
return EvaluationResult::valid_empty();
}
let mut result = EvaluationResult::valid_empty();
result.annotate(Annotations::from_arc(Arc::clone(&self.annotation)));
result
}
}
pub(crate) trait Format: Send + Sync + 'static {
fn is_valid(&self, value: &str) -> bool;
}
impl<F> Format for F
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
#[inline]
fn is_valid(&self, value: &str) -> bool {
self(value)
}
}
#[inline]
pub(crate) fn compile<'a>(
ctx: &compiler::Context,
_: &'a Map<String, Value>,
schema: &'a Value,
) -> Option<CompilationResult<'a>> {
if let Value::String(format) = schema {
if ctx.validates_formats_by_default() {
if let Some((name, func)) = ctx.get_format(format) {
return Some(CustomFormatValidator::compile(
ctx,
name.clone(),
func.clone(),
));
}
let draft = ctx.draft();
match format.as_str() {
"date" => Some(DateValidator::compile(ctx)),
"date-time" => Some(DateTimeValidator::compile(ctx)),
"duration" if draft >= Draft::Draft201909 => Some(DurationValidator::compile(ctx)),
"email" => Some(EmailValidator::compile(ctx)),
"hostname" if matches!(draft, Draft::Draft4 | Draft::Draft6) => {
Some(HostnameValidatorDraft4::compile(ctx))
}
"hostname" => Some(HostnameValidator::compile(ctx)),
"idn-email" => Some(IdnEmailValidator::compile(ctx)),
"idn-hostname" if draft >= Draft::Draft7 => {
Some(IdnHostnameValidator::compile(ctx))
}
"ipv4" => Some(IpV4Validator::compile(ctx)),
"ipv6" => Some(IpV6Validator::compile(ctx)),
"iri" if draft >= Draft::Draft7 => Some(IriValidator::compile(ctx)),
"iri-reference" if draft >= Draft::Draft7 => {
Some(IriReferenceValidator::compile(ctx))
}
"json-pointer" if draft >= Draft::Draft6 => {
Some(JsonPointerValidator::compile(ctx))
}
"regex" => Some(RegexValidator::compile(ctx)),
"relative-json-pointer" if draft >= Draft::Draft7 => {
Some(RelativeJsonPointerValidator::compile(ctx))
}
"time" => Some(TimeValidator::compile(ctx)),
"uri" => Some(UriValidator::compile(ctx)),
"uri-reference" if draft >= Draft::Draft6 => {
Some(UriReferenceValidator::compile(ctx))
}
"uri-template" if draft >= Draft::Draft6 => {
Some(UriTemplateValidator::compile(ctx))
}
"uuid" if draft >= Draft::Draft201909 => Some(UuidValidator::compile(ctx)),
name => {
if ctx.are_unknown_formats_ignored() {
None
} else {
let location = ctx.location().join("format");
Some(Err(ValidationError::compile_error(
location.clone(),
location,
Location::new(),
schema,
format!(
"Unknown format: '{name}'. Adjust configuration to ignore unrecognized formats"
),
)))
}
}
}
} else {
Some(Ok(Box::new(AnnotationOnlyFormatValidator {
annotation: Arc::new(Value::String(format.clone())),
})))
}
} else {
let location = ctx.location().join("format");
Some(Err(ValidationError::single_type_error(
location.clone(),
location,
Location::new(),
schema,
JsonType::String,
)))
}
}
#[cfg(test)]
mod tests {
use referencing::Draft;
use serde_json::json;
use test_case::test_case;
use crate::{tests_util, EmailOptions};
use super::*;
#[test_case(b"00" => Some(0); "min")]
#[test_case(b"09" => Some(9); "nine")]
#[test_case(b"10" => Some(10); "ten")]
#[test_case(b"59" => Some(59); "fifty-nine")]
#[test_case(b"99" => Some(99); "max")]
#[test_case(b"1a" => None; "alpha")]
#[test_case(b" 5" => None; "leading space")]
#[test_case(b":0" => None; "colon is not a digit")]
#[test_case(b";9" => None; "semicolon is not a digit")]
fn test_parse_two_digits(bytes: &[u8]) -> Option<u8> {
parse_two_digits(bytes)
}
#[test_case(b"0000" => Some(0); "zero")]
#[test_case(b"1970" => Some(1970); "epoch year")]
#[test_case(b"2023" => Some(2023); "recent year")]
#[test_case(b"9999" => Some(9999); "max")]
#[test_case(b"199x" => None; "trailing alpha")]
#[test_case(b" 999" => None; "leading space")]
#[test_case(b"20:3" => None; "colon is not a digit")]
fn test_parse_four_digits(bytes: &[u8]) -> Option<u16> {
parse_four_digits(bytes)
}
#[test]
fn ignored_format() {
let schema = json!({"format": "custom", "type": "string"});
let instance = json!("foo");
let validator = crate::validator_for(&schema).unwrap();
assert!(validator.is_valid(&instance));
}
#[test]
fn format_validation() {
let schema = json!({"format": "email", "type": "string"});
let email_instance = json!("email@example.com");
let not_email_instance = json!("foo");
let with_validation = crate::options()
.should_validate_formats(true)
.build(&schema)
.unwrap();
let without_validation = crate::options()
.should_validate_formats(false)
.build(&schema)
.unwrap();
assert!(with_validation.is_valid(&email_instance));
assert!(!with_validation.is_valid(¬_email_instance));
assert!(without_validation.is_valid(&email_instance));
assert!(without_validation.is_valid(¬_email_instance));
}
#[test]
fn ecma_regex() {
let schema = json!({"format": "regex", "type": "string"});
let instance = json!("^\\cc$");
let validator = crate::validator_for(&schema).unwrap();
assert!(validator.is_valid(&instance));
}
#[test]
fn location() {
tests_util::assert_schema_location(&json!({"format": "date"}), &json!("bla"), "/format");
}
#[test]
fn uuid() {
let schema = json!({"format": "uuid", "type": "string"});
let passing_instance = json!("f308a72c-fa84-11eb-9a03-0242ac130003");
let failing_instance = json!("1");
let validator = crate::options()
.with_draft(Draft::Draft201909)
.should_validate_formats(true)
.build(&schema)
.unwrap();
assert!(validator.is_valid(&passing_instance));
assert!(!validator.is_valid(&failing_instance));
}
#[test]
fn uri() {
let schema = json!({"format": "uri", "type": "string"});
let passing_instance = json!("https://phillip.com");
let failing_instance = json!("redis");
tests_util::is_valid(&schema, &passing_instance);
tests_util::is_not_valid(&schema, &failing_instance);
}
#[test_case("P1Y1Y")]
#[test_case("PT1H1H")]
fn test_invalid_duration(input: &str) {
assert!(!is_valid_duration(input));
}
#[test]
fn unknown_formats_should_not_be_ignored() {
let schema = json!({ "format": "custom", "type": "string"});
let error = crate::options()
.should_validate_formats(true)
.should_ignore_unknown_formats(false)
.build(&schema)
.expect_err("the validation error should be returned");
assert_eq!(
error.to_string(),
"Unknown format: 'custom'. Adjust configuration to ignore unrecognized formats"
);
}
#[test_case("2023-01-01", true; "valid regular date")]
#[test_case("2020-02-29", true; "valid leap year date")]
#[test_case("2021-02-28", true; "valid non-leap year date")]
#[test_case("1900-02-28", true; "valid century non-leap year")]
#[test_case("2000-02-29", true; "valid leap century year")]
#[test_case("1999-12-31", true; "valid end of year date")]
#[test_case("202-12-01", false; "invalid short year")]
#[test_case("2023-1-01", false; "invalid short month")]
#[test_case("2023-12-1", false; "invalid short day")]
#[test_case("2023/12/01", false; "invalid separators")]
#[test_case("2023-13-01", false; "invalid month too high")]
#[test_case("2023-00-01", false; "invalid month too low")]
#[test_case("2023-12-32", false; "invalid day too high")]
#[test_case("2023-11-31", false; "invalid day for 30-day month")]
#[test_case("2023-02-30", false; "invalid day for February in non-leap year")]
#[test_case("2021-02-29", false; "invalid day for non-leap year")]
#[test_case("2023-12-00", false; "invalid day too low")]
#[test_case("99999-12-01", false; "year too long")]
#[test_case("1900-02-29", false; "invalid leap century non-leap year")]
#[test_case("2000-02-30", false; "invalid day for leap century year")]
#[test_case("2400-02-29", true; "valid leap year in distant future")]
#[test_case("0000-01-01", true; "valid boundary start date")]
#[test_case("9999-12-31", true; "valid boundary end date")]
#[test_case("aaaa-01-12", false; "Malformed (letters in year)")]
#[test_case("2000-bb-12", false; "Malformed (letters in month)")]
#[test_case("2000-01-cc", false; "Malformed (letters in day)")]
#[test_case("20:3-01-15", false; "colon in year")]
#[test_case("20;3-01-15", false; "semicolon in year")]
fn test_is_valid_date(input: &str, expected: bool) {
assert_eq!(is_valid_date(input), expected);
}
#[test_case("23:59:59Z", true; "valid time with Z")]
#[test_case("00:00:00Z", true; "valid midnight time with Z")]
#[test_case("12:30:45.123Z", true; "valid time with fractional seconds and Z")]
#[test_case("23:59:60Z", true; "valid leap second UTC time")]
#[test_case("12:30:45+01:00", true; "valid time with positive offset")]
#[test_case("12:30:45-01:00", true; "valid time with negative offset")]
#[test_case("23:59:60+00:00", true; "valid leap second with offset UTC 00:00")]
#[test_case("23:59:59+01:00", true; "valid time with +01:00 offset")]
#[test_case("23:59:59A", false; "invalid time with non-Z/non-offset letter")]
#[test_case("12:3:45Z", false; "invalid time with missing digit in minute")]
#[test_case("12:30:4Z", false; "invalid time with missing digit in second")]
#[test_case("12-30-45Z", false; "invalid time with wrong separator")]
#[test_case("12:30:45Z+01:00", false; "invalid time with Z and offset together")]
#[test_case("12:30:45A01:00", false; "invalid time with wrong separator between time and offset")]
#[test_case("12:30:45++01:00", false; "invalid double plus in offset")]
#[test_case("12:30:45+01:60", false; "invalid minute in offset")]
#[test_case("12:30:45+24:00", false; "invalid hour in offset")]
#[test_case("12:30:45.", false; "invalid time with incomplete fractional second")]
#[test_case("24:00:00Z", false; "invalid hour > 23")]
#[test_case("12:60:00Z", false; "invalid minute > 59")]
#[test_case("12:30:61Z", false; "invalid second > 60")]
#[test_case("12:30:60+01:00", false; "invalid leap second with non-UTC offset")]
#[test_case("23:59:60Z+01:00", false; "invalid leap second with non-zero offset")]
#[test_case("23:59:60+00:30", false; "invalid leap second with non-zero minute offset")]
#[test_case("23:59:60Z", true; "valid leap second at the end of day")]
#[test_case("23:59:60+00:00", true; "valid leap second with zero offset")]
#[test_case("ab:59:59Z", false; "invalid time with letters in hour")]
#[test_case("23:ab:59Z", false; "invalid time with letters in minute")]
#[test_case("23:59:abZ", false; "invalid time with letters in second")]
#[test_case("23:59:59aZ", false; "invalid time with letter after seconds")]
#[test_case("12:30:45+ab:00", false; "invalid offset hour with letters")]
#[test_case("12:30:45+01:ab", false; "invalid offset minute with letters")]
#[test_case("12:30:45.abcZ", false; "invalid fractional seconds with letters")]
fn test_is_valid_time(input: &str, expected: bool) {
assert_eq!(is_valid_time(input), expected);
}
#[test]
fn test_is_valid_datetime() {
assert!(!is_valid_datetime(""));
}
#[test_case("127.0.0.1", true)]
#[test_case("192.168.1.1", true)]
#[test_case("10.0.0.1", true)]
#[test_case("0.0.0.0", true)]
#[test_case("256.1.2.3", false; "first octet too large")]
#[test_case("1.256.3.4", false; "second octet too large")]
#[test_case("1.2.256.4", false; "third octet too large")]
#[test_case("1.2.3.256", false; "fourth octet too large")]
#[test_case("01.2.3.4", false; "leading zero in first octet")]
#[test_case("1.02.3.4", false; "leading zero in second octet")]
#[test_case("1.2.03.4", false; "leading zero in third octet")]
#[test_case("1.2.3.04", false; "leading zero in fourth octet")]
#[test_case("1.2.3", false; "too few octets")]
#[test_case("1.2.3.4.5", false; "too many octets")]
fn ip_v4(input: &str, expected: bool) {
let validator = crate::options()
.should_validate_formats(true)
.build(&json!({"format": "ipv4", "type": "string"}))
.expect("Invalid schema");
assert_eq!(validator.is_valid(&json!(input)), expected);
}
#[test]
fn test_is_valid_datetime_panic() {
is_valid_datetime("2624-04-25t23:14:04-256\x112");
}
#[test_case("example.com" ; "simple valid hostname")]
#[test_case("xn--bcher-kva.com" ; "valid punycode")]
#[test_case("münchen.de" ; "valid IDN")]
#[test_case("test\u{094D}\u{200D}example.com" ; "valid zero width joiner after virama")]
#[test_case("۱۲۳.example.com" ; "valid extended arabic-indic digits")]
#[test_case("ひらがな・カタカナ.com" ; "valid katakana middle dot")]
fn test_valid_idn_hostnames(input: &str) {
assert!(is_valid_idn_hostname(input));
}
#[test_case("xn--ll-0ea" ; "punycode with valid middle dot context")]
#[test_case("xn--11b2ezcw70k" ; "zero width joiner preceded by virama")]
fn test_valid_punycode_hostnames(input: &str) {
assert!(is_valid_hostname(input));
}
#[test_case("ex--ample.com" ; "hyphen at 3rd & 4th position")]
#[test_case("-example.com" ; "leading hyphen")]
#[test_case("example-.com" ; "trailing hyphen")]
#[test_case("xn--example.com" ; "invalid punycode")]
#[test_case("xn--x" ; "too short punycode label")]
#[test_case("xn--vek" ; "katakana middle dot without companions")]
#[test_case("xn--l-fda" ; "middle dot with nothing preceding")]
#[test_case("xn--l-gda" ; "middle dot with nothing following")]
#[test_case("xn--02b508i" ; "zero width joiner not preceded by virama")]
#[test_case("xn--a-2hc5h" ; "hebrew geresh not preceded by hebrew")]
#[test_case("xn--a-2hc8h" ; "hebrew gershayim not preceded by hebrew")]
#[test_case("test\u{200D}example.com" ; "zero width joiner not after virama")]
#[test_case("test\u{0061}\u{200D}example.com" ; "zero width joiner after non-virama")]
#[test_case("" ; "empty string")]
#[test_case("." ; "single dot")]
#[test_case("example..com" ; "consecutive dots")]
#[test_case("exa mple.com" ; "contains space")]
#[test_case("example.com." ; "trailing dot")]
#[test_case("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com" ; "too long")]
#[test_case("xn--bcher-.com" ; "invalid punycode with hyphen")]
#[test_case("١۲٣.example.com" ; "mixed arabic-indic digits")]
#[test_case("example・com" ; "katakana middle dot without hiragana/katakana/han")]
fn test_invalid_idn_hostnames(input: &str) {
assert!(!is_valid_idn_hostname(input));
}
#[test_case("xn--l-fda" ; "middle dot with nothing preceding")]
#[test_case("xn--l-gda" ; "middle dot with nothing following")]
#[test_case("xn--02b508i" ; "zero width joiner not preceded by anything")]
#[test_case("xn--11b2er09f" ; "zero width joiner not preceded by virama")]
#[test_case("xn--hello-zed" ; "punycode beginning with nonspacing mark")]
#[test_case("xn--hello-txk" ; "punycode beginning with spacing combining mark")]
#[test_case("xn--hello-6bf" ; "punycode beginning with enclosing mark")]
#[test_case("XN--aa---o47jg78q" ; "uppercase punycode prefix rejected")]
fn test_invalid_punycode_hostnames(input: &str) {
assert!(!is_valid_hostname(input));
}
#[test_case(Draft::Draft4 ; "draft4")]
#[test_case(Draft::Draft6 ; "draft6")]
fn test_hostname_a_label_rules_not_applied_in_legacy_drafts(draft: Draft) {
let schema = json!({"format": "hostname", "type": "string"});
let validator = crate::options()
.with_draft(draft)
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("XN--9krT00a")));
assert!(validator.is_valid(&json!("ex--ample.com")));
}
#[test_case(Draft::Draft7 ; "draft7")]
#[test_case(Draft::Draft201909 ; "draft2019-09")]
#[test_case(Draft::Draft202012 ; "draft2020-12")]
fn test_hostname_a_label_rules_applied_in_modern_drafts(draft: Draft) {
let schema = json!({"format": "hostname", "type": "string"});
let validator = crate::options()
.with_draft(draft)
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(!validator.is_valid(&json!("XN--9krT00a")));
assert!(!validator.is_valid(&json!("ex--ample.com")));
}
#[test]
fn test_invalid_hostname() {
assert!(!is_valid_hostname("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com"));
}
#[test_case(""; "empty string")]
#[test_case("/"; "root")]
#[test_case("/foo"; "simple key")]
#[test_case("/foo/0"; "array index")]
#[test_case("/foo/bar"; "nested keys")]
#[test_case("/f~0o/b~1r"; "escaped characters")]
#[test_case("/foo/bar/"; "trailing slash")]
#[test_case("/foo//bar"; "empty reference token")]
fn test_valid_json_pointer(pointer: &str) {
assert!(is_valid_json_pointer(pointer));
}
#[test_case("foo"; "missing leading slash")]
#[test_case("/foo/~"; "incomplete escape")]
#[test_case("/foo/~2"; "invalid escape")]
#[test_case("/foo\x7E"; "unescaped tilde")]
fn test_invalid_json_pointer(pointer: &str) {
assert!(!is_valid_json_pointer(pointer));
}
#[test_case("0"; "zero")]
#[test_case("1"; "positive integer")]
#[test_case("10"; "multi-digit integer")]
#[test_case("0#"; "zero with hash")]
#[test_case("1#"; "positive integer with hash")]
#[test_case("0/"; "zero with slash")]
#[test_case("1/foo"; "integer with json pointer")]
#[test_case("10/foo/bar"; "multi-digit integer with json pointer")]
fn test_valid_relative_json_pointer(pointer: &str) {
assert!(is_valid_relative_json_pointer(pointer));
}
#[test_case(""; "empty string")]
#[test_case("-1"; "negative integer")]
#[test_case("01"; "leading zero")]
#[test_case("1.5"; "decimal")]
#[test_case("a"; "non-digit")]
#[test_case("1a"; "digit followed by non-digit")]
#[test_case("1#/"; "hash not at end")]
#[test_case("1/~"; "incomplete escape in json pointer")]
fn test_invalid_relative_json_pointer(pointer: &str) {
assert!(!is_valid_relative_json_pointer(pointer));
}
#[test]
fn email_options_backward_compatibility() {
let schema = json!({"format": "email", "type": "string"});
let validator = crate::options()
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("missing@domain")));
assert!(validator.is_valid(&json!("user@example.com")));
assert!(!validator.is_valid(&json!("not-an-email")));
}
#[test]
fn email_options_custom() {
let schema = json!({"format": "email", "type": "string"});
let validator = crate::options()
.with_email_options(EmailOptions::default())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("user@example.com")));
assert!(!validator.is_valid(&json!("not-an-email")));
}
#[test]
fn email_options_default() {
let schema = json!({"format": "email", "type": "string"});
let validator = crate::options()
.with_email_options(EmailOptions::default())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("user@example.com")));
assert!(!validator.is_valid(&json!("not-an-email")));
}
#[test]
fn idn_email_options() {
let schema = json!({"format": "idn-email", "type": "string"});
let validator = crate::options()
.with_email_options(EmailOptions::default())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("user@example.com")));
assert!(!validator.is_valid(&json!("not-an-email")));
}
#[test]
fn email_options_minimum_sub_domains() {
let schema = json!({"format": "email", "type": "string"});
let validator = crate::options()
.with_email_options(EmailOptions::default().with_no_minimum_sub_domains())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("simon@localhost")));
assert!(validator.is_valid(&json!("user@example.com")));
let validator = crate::options()
.with_email_options(EmailOptions::default().with_required_tld())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(!validator.is_valid(&json!("simon@localhost")));
assert!(validator.is_valid(&json!("user@example.com")));
let validator = crate::options()
.with_email_options(EmailOptions::default().with_minimum_sub_domains(3))
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(!validator.is_valid(&json!("user@example.com")));
assert!(validator.is_valid(&json!("user@sub.example.com")));
}
#[test]
fn email_options_domain_literal() {
let schema = json!({"format": "email", "type": "string"});
let validator = crate::options()
.with_email_options(EmailOptions::default().with_domain_literal())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("email@[127.0.0.1]")));
assert!(validator.is_valid(&json!("email@[IPv6:2001:db8::1]")));
let validator = crate::options()
.with_email_options(EmailOptions::default().without_domain_literal())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(!validator.is_valid(&json!("email@[127.0.0.1]")));
assert!(!validator.is_valid(&json!("email@[IPv6:2001:db8::1]")));
assert!(validator.is_valid(&json!("user@example.com")));
}
#[test]
fn email_options_display_text() {
let schema = json!({"format": "email", "type": "string"});
let validator = crate::options()
.with_email_options(EmailOptions::default().with_display_text())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(validator.is_valid(&json!("Simon <simon@example.com>")));
assert!(validator.is_valid(&json!("simon@example.com")));
let validator = crate::options()
.with_email_options(EmailOptions::default().without_display_text())
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(!validator.is_valid(&json!("Simon <simon@example.com>")));
assert!(!validator.is_valid(&json!("<simon@example.com>")));
assert!(validator.is_valid(&json!("simon@example.com")));
}
#[test]
fn email_options_combined() {
let schema = json!({"format": "email", "type": "string"});
let validator = crate::options()
.with_email_options(
EmailOptions::default()
.with_required_tld()
.without_domain_literal()
.without_display_text(),
)
.should_validate_formats(true)
.build(&schema)
.expect("Schema should compile");
assert!(!validator.is_valid(&json!("user@localhost")));
assert!(!validator.is_valid(&json!("user@[127.0.0.1]")));
assert!(!validator.is_valid(&json!("Name <user@example.com>")));
assert!(validator.is_valid(&json!("user@example.com")));
}
#[test_case(""; "empty string")]
#[test_case("http://example.com/"; "plain URL")]
#[test_case("no-template-here"; "no templates")]
#[test_case("{var}"; "simple variable")]
#[test_case("http://example.com/{var}"; "URL with variable")]
#[test_case("http://example.com/dictionary/{term}"; "URL with named variable")]
#[test_case("/users/{id}"; "path with variable")]
#[test_case("http://example.com/~{username}/"; "URL with tilde")]
#[test_case("{+var}"; "reserved expansion")]
#[test_case("{#var}"; "fragment expansion")]
#[test_case("{.var}"; "label expansion with dot")]
#[test_case("{/var}"; "path segment expansion")]
#[test_case("{;var}"; "path-style parameter expansion")]
#[test_case("{?var}"; "query expansion")]
#[test_case("{&var}"; "query continuation expansion")]
#[test_case("{=var}"; "reserved operator equals")]
#[test_case("{,var}"; "reserved operator comma")]
#[test_case("{!var}"; "reserved operator exclamation")]
#[test_case("{@var}"; "reserved operator at")]
#[test_case("{|var}"; "reserved operator pipe")]
#[test_case("{|var*}"; "reserved operator with explode")]
#[test_case("{keys:1}"; "prefix modifier on any var")]
#[test_case("{+keys:1}"; "operator with prefix modifier")]
#[test_case("{var:10}"; "prefix modifier")]
#[test_case("{var:1}"; "prefix modifier min")]
#[test_case("{var:9999}"; "prefix modifier max")]
#[test_case("{var*}"; "explode modifier")]
#[test_case("{+var*}"; "operator with explode")]
#[test_case("{#var:5}"; "operator with prefix")]
#[test_case("{var1,var2}"; "multiple variables")]
#[test_case("{var1,var2,var3}"; "three variables")]
#[test_case("{+var1,var2}"; "operator with multiple variables")]
#[test_case("{var1:5,var2*}"; "multiple variables with modifiers")]
#[test_case("http://example.com{+path}{?query*}"; "complex template")]
#[test_case("http://example.com{#fragment}"; "fragment template")]
#[test_case("http://example.com{.dom*}"; "domain template")]
#[test_case("http://example.com{/path,path2}"; "path template")]
#[test_case("http://example.com{;params*}"; "params template")]
#[test_case("http://example.com{?query,query2}"; "query template")]
#[test_case("http://example.com/{var1}/{var2}/{var3}"; "multiple expressions")]
#[test_case("{var.name}"; "dotted variable name")]
#[test_case("{a.b.c}"; "multiple dots in variable name")]
#[test_case("{%20}"; "percent-encoded space in varname")]
#[test_case("{a%20b}"; "percent-encoded in middle of varname")]
#[test_case("{%41}"; "percent-encoded A")]
#[test_case("http://example.com/%20space"; "percent-encoded in URL")]
#[test_case("hello%20world"; "percent-encoded space")]
fn test_valid_uri_template(template: &str) {
assert!(is_valid_uri_template(template));
}
#[test_case("{"; "unclosed brace")]
#[test_case("}"; "unmatched close brace")]
#[test_case("{}"; "empty expression")]
#[test_case("{+}"; "operator only")]
#[test_case("http://example.com/{unclosed"; "unclosed in URL")]
#[test_case("http://example.com/{var"; "missing close brace")]
#[test_case("http://example.com/}"; "extra close brace")]
#[test_case("{var:0}"; "prefix zero")]
#[test_case("{var:}"; "prefix empty")]
#[test_case("{var:10000}"; "prefix too large")]
#[test_case("{var::5}"; "double colon")]
#[test_case("{var**}"; "double explode")]
#[test_case("{*}"; "explode only")]
#[test_case("{:5}"; "prefix only")]
#[test_case("{-var}"; "hyphen start")]
#[test_case("{var-}"; "hyphen in variable")]
#[test_case("{.}"; "dot only")]
#[test_case("{..var}"; "double dot start")]
#[test_case("{var..name}"; "double dot in name")]
#[test_case("{%}"; "incomplete percent")]
#[test_case("{%Z}"; "incomplete percent hex")]
#[test_case("{%ZZ}"; "invalid hex digits")]
#[test_case("{%0}"; "single hex digit")]
#[test_case("%"; "incomplete percent in literal")]
#[test_case("%Z"; "incomplete percent in literal 2")]
#[test_case("%ZZ"; "invalid hex in literal")]
#[test_case("hello world"; "space in literal")]
#[test_case("hello\ttab"; "tab in literal")]
#[test_case("hello\nline"; "newline in literal")]
#[test_case("hello\"quote"; "quote in literal")]
#[test_case("hello<angle"; "angle bracket in literal")]
#[test_case("hello\\back"; "backslash in literal")]
#[test_case("hello^caret"; "caret in literal")]
#[test_case("hello`backtick"; "backtick in literal")]
#[test_case("hello|pipe"; "pipe in literal")]
#[test_case("{var,}"; "trailing comma")]
#[test_case("{var,,var2}"; "double comma")]
#[test_case("{/id*"; "uritemplate-test 1: unclosed brace")]
#[test_case("/id*}"; "uritemplate-test 2: unmatched close brace")]
#[test_case("{/?id}"; "uritemplate-test 3: question mark in varname")]
#[test_case("{var:prefix}"; "uritemplate-test 4: non-numeric prefix")]
#[test_case("{hello:2*}"; "uritemplate-test 5: prefix and explode combined")]
#[test_case("{??hello}"; "uritemplate-test 6: question mark in varname")]
#[test_case("{with space}"; "uritemplate-test 8: space in variable name")]
#[test_case("{ leading_space}"; "uritemplate-test 9: leading space in expression")]
#[test_case("{trailing_space }"; "uritemplate-test 10: trailing space in expression")]
#[test_case("{$var}"; "uritemplate-test 12: dollar sign not a valid operator")]
#[test_case("{*keys?}"; "uritemplate-test 14: explode at start of varspec")]
#[test_case("{?empty=default,var}"; "uritemplate-test 15: equals sign in varname")]
#[test_case("{var}{-prefix|/-/|var}"; "uritemplate-test 16: hyphen not a valid operator")]
#[test_case("?q={searchTerms}&c={example:color?}"; "uritemplate-test 17: non-numeric prefix")]
#[test_case("x{?empty|foo=none}"; "uritemplate-test 18: invalid chars after varname")]
#[test_case("/h{#hello+}"; "uritemplate-test 19: plus in varname")]
#[test_case("/h#{hello+}"; "uritemplate-test 20: plus in varname")]
#[test_case("{;keys:1*}"; "uritemplate-test 23: prefix and explode combined")]
#[test_case("?{-join|&|var,list}"; "uritemplate-test 24: hyphen not a valid operator")]
#[test_case("{~thing}"; "uritemplate-test 25: tilde not a valid operator")]
#[test_case("/{default-graph-uri}"; "uritemplate-test 26: hyphen in varname")]
#[test_case("/sparql{?query,default-graph-uri}"; "uritemplate-test 27: hyphen in varname")]
#[test_case("/sparql{?query){&default-graph-uri*}"; "uritemplate-test 28: paren in template")]
#[test_case("/resolution{?x, y}"; "uritemplate-test 29: space after comma")]
fn test_invalid_uri_template(template: &str) {
assert!(!is_valid_uri_template(template));
}
}