use std::{
borrow::Cow,
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
sync::Arc,
};
use email_address::{EmailAddress, Options as EmailAddressOptions};
use serde_json::{Map, Value};
use strum::VariantArray;
use unicode_general_category::{get_general_category, GeneralCategory};
use uuid_simd::{parse_hyphenated, Out};
use crate::{
compiler,
error::ValidationError,
evaluation::Annotations,
keywords::CompilationResult,
paths::{LazyLocation, Location, RefTracker},
types::JsonType,
validator::{EvaluationResult, Validate, ValidationContext},
Draft, Json, Node,
};
#[must_use]
pub 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'}' | 0x7F
)
}
#[must_use]
pub 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)
}
#[must_use]
pub 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
}
#[must_use]
pub 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
}
}};
}
#[must_use]
pub 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,
}
}
#[must_use]
pub 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 parse_email(email: &str, options: Option<&EmailAddressOptions>) -> Option<EmailAddress> {
if let Some(opts) = options {
EmailAddress::parse_with_options(email, *opts)
} else {
EmailAddress::from_str(email)
}
.ok()
}
const IPV6_TAG: &str = "IPv6:";
fn validate_email_domain<F>(domain: &str, is_valid_hostname_impl: &F) -> bool
where
F: Fn(&str) -> bool,
{
if let Some(domain) = domain.strip_prefix('[').and_then(|d| d.strip_suffix(']')) {
if domain
.as_bytes()
.get(..IPV6_TAG.len())
.is_some_and(|tag| tag.eq_ignore_ascii_case(IPV6_TAG.as_bytes()))
{
domain[IPV6_TAG.len()..].parse::<Ipv6Addr>().is_ok()
} else {
domain.parse::<Ipv4Addr>().is_ok()
}
} else {
is_valid_hostname_impl(domain)
}
}
fn is_valid_email_impl<F>(
email: &str,
is_valid_hostname_impl: F,
options: Option<&EmailAddressOptions>,
allow_non_ascii_local_part: bool,
) -> bool
where
F: Fn(&str) -> bool,
{
if let Some(domain) = email.strip_prefix("\"\"@") {
return is_valid_email_impl(
&format!("\"a\"@{domain}"),
is_valid_hostname_impl,
options,
allow_non_ascii_local_part,
);
}
if let Some(parsed) = parse_email(email, options) {
if !allow_non_ascii_local_part && !parsed.local_part().is_ascii() {
return false;
}
return validate_email_domain(parsed.domain(), &is_valid_hostname_impl);
}
if !allow_non_ascii_local_part || email.is_ascii() {
return false;
}
let masked: String = email
.chars()
.map(|c| if c.is_ascii() { c } else { 'a' })
.collect();
if parse_email(&masked, options).is_none() {
return false;
}
let mut parts = email.rsplitn(2, '@');
let domain = parts.next().unwrap_or_default();
if parts.next().is_none() {
return false;
}
validate_email_domain(domain, &is_valid_hostname_impl)
}
pub(crate) fn is_valid_email(email: &str, options: Option<&EmailAddressOptions>) -> bool {
is_valid_email_impl(email, is_valid_hostname, options, false)
}
#[cfg(feature = "idna")]
pub(crate) fn is_valid_idn_email(email: &str, options: Option<&EmailAddressOptions>) -> bool {
is_valid_email_impl(email, is_valid_idn_hostname, options, true)
}
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..])
}
#[must_use]
pub fn is_valid_hostname_rfc1034(hostname: &str) -> bool {
is_valid_ascii_hostname(hostname)
}
const PUNYCODE_BASE: u32 = 36;
const PUNYCODE_TMIN: u32 = 1;
const PUNYCODE_TMAX: u32 = 26;
const PUNYCODE_SKEW: u32 = 38;
const PUNYCODE_DAMP: u32 = 700;
const PUNYCODE_INITIAL_BIAS: u32 = 72;
const PUNYCODE_INITIAL_N: u32 = 128;
fn punycode_digit(byte: u8) -> Option<u32> {
match byte {
b'0'..=b'9' => Some(u32::from(byte) - u32::from(b'0') + 26),
b'A'..=b'Z' => Some(u32::from(byte) - u32::from(b'A')),
b'a'..=b'z' => Some(u32::from(byte) - u32::from(b'a')),
_ => None,
}
}
fn punycode_adapt(mut delta: u32, count: u32, first: bool) -> u32 {
delta /= if first { PUNYCODE_DAMP } else { 2 };
delta += delta / count;
let mut k = 0;
while delta > ((PUNYCODE_BASE - PUNYCODE_TMIN) * PUNYCODE_TMAX) / 2 {
delta /= PUNYCODE_BASE - PUNYCODE_TMIN;
k += PUNYCODE_BASE;
}
k + ((PUNYCODE_BASE - PUNYCODE_TMIN + 1) * delta) / (delta + PUNYCODE_SKEW)
}
fn decode_punycode(input: &str) -> Option<String> {
let bytes = input.as_bytes();
let (basic, encoded) = match bytes.iter().rposition(|&byte| byte == b'-') {
Some(position) if position > 0 => (&bytes[..position], &bytes[position + 1..]),
_ => (&bytes[..0], bytes),
};
if !basic.is_ascii() {
return None;
}
let mut output: Vec<char> = basic.iter().map(|&byte| char::from(byte)).collect();
let mut code_point = PUNYCODE_INITIAL_N;
let mut index: u32 = 0;
let mut bias = PUNYCODE_INITIAL_BIAS;
let mut position = 0;
while position < encoded.len() {
let previous = index;
let mut weight: u32 = 1;
let mut k = PUNYCODE_BASE;
loop {
let digit = punycode_digit(*encoded.get(position)?)?;
position += 1;
index = index.checked_add(digit.checked_mul(weight)?)?;
let threshold = if k <= bias {
PUNYCODE_TMIN
} else if k >= bias + PUNYCODE_TMAX {
PUNYCODE_TMAX
} else {
k - bias
};
if digit < threshold {
break;
}
weight = weight.checked_mul(PUNYCODE_BASE - threshold)?;
k += PUNYCODE_BASE;
}
let count = u32::try_from(output.len()).ok()? + 1;
bias = punycode_adapt(index - previous, count, previous == 0);
code_point = code_point.checked_add(index / count)?;
index %= count;
output.insert(index as usize, char::from_u32(code_point)?);
index += 1;
}
Some(output.into_iter().collect())
}
#[must_use]
pub 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) = decode_punycode(payload) else {
return false;
};
if !validate_unicode_label(&decoded) {
return false;
}
}
}
true
}
fn is_idna_pvalid_category(category: GeneralCategory) -> bool {
matches!(
category,
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
| GeneralCategory::NonspacingMark
| GeneralCategory::SpacingMark
| GeneralCategory::DecimalNumber
)
}
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,
'\u{200C}' | '\u{200D}' | '\u{00B7}' | '\u{0375}' | '\u{05F3}' | '\u{05F4}'
| '\u{06FD}' | '\u{06FE}' | '\u{0F0B}' | '\u{3007}' => {}
other if !other.is_ascii() && !is_idna_pvalid_category(get_general_category(other)) => {
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
}
#[cfg(feature = "idna")]
#[must_use]
pub 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)
}
#[must_use]
pub 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"YMWD";
let time_units = *b"HMS";
while i < len {
if bytes[i] == b'T' {
if has_time || has_weeks {
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
}
#[must_use]
pub fn is_valid_ipv4(ip: &str) -> bool {
Ipv4Addr::from_str(ip).is_ok()
}
#[must_use]
pub fn is_valid_ipv6(ip: &str) -> bool {
Ipv6Addr::from_str(ip).is_ok()
}
#[must_use]
pub fn is_valid_iri(iri: &str) -> bool {
referencing::Iri::parse(iri).is_ok()
}
#[must_use]
pub fn is_valid_iri_reference(iri_reference: &str) -> bool {
referencing::IriRef::parse(iri_reference).is_ok()
}
#[must_use]
pub fn is_valid_uri(uri: &str) -> bool {
referencing::Uri::parse(uri).is_ok()
}
#[must_use]
pub fn is_valid_uri_reference(uri_reference: &str) -> bool {
referencing::UriRef::parse(uri_reference).is_ok()
}
#[must_use]
pub fn is_valid_uuid(uuid: &str) -> bool {
let mut out = [0; 16];
parse_hyphenated(uuid.as_bytes(), Out::from_mut(&mut out)).is_ok()
}
#[must_use]
pub fn is_valid_regex(pattern: &str) -> bool {
const CACHE_SIZE: usize = 16;
thread_local! {
static CACHE: std::cell::RefCell<(Vec<(String, bool)>, usize)> =
const { std::cell::RefCell::new((Vec::new(), 0)) };
}
CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let (entries, next) = &mut *cache;
if let Some((_, valid)) = entries.iter().find(|(candidate, _)| candidate == pattern) {
return *valid;
}
let valid = jsonschema_regex::is_valid_ecma_regex(pattern);
if entries.len() < CACHE_SIZE {
entries.push((pattern.to_owned(), valid));
} else {
entries[*next] = (pattern.to_owned(), valid);
*next = (*next + 1) % CACHE_SIZE;
}
valid
})
}
macro_rules! impl_format_evaluate {
() => {
fn evaluate(
&self,
instance: &F::Node<'_>,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> EvaluationResult {
if !instance.is_string() {
return EvaluationResult::valid_empty();
}
let mut collected = Vec::new();
Validate::<F>::collect_errors(self, instance, location, tracker, ctx, &mut collected);
let errors: Vec<_> = collected
.iter()
.map(crate::evaluation::ErrorDescription::from_validation_error)
.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 {
($($(#[$meta:meta])* ($validator:ident, $format:expr, $validation_fn:ident)),+ $(,)?) => {
$(
$(#[$meta])*
struct $validator {
location: Location,
annotation: Arc<Value>,
}
$(#[$meta])*
impl $validator {
pub(crate) fn compile<'a, F: Json>(ctx: &compiler::Context<F>) -> CompilationResult<'a, F> {
let location = ctx.location().join("format");
let annotation = Arc::new(Value::String($format.to_owned()));
Ok(Box::new($validator { location, annotation }))
}
}
$(#[$meta])*
impl<F: Json> Validate<F> for $validator {
fn is_valid(&self, instance: &F::Node<'_>, _ctx: &mut ValidationContext) -> bool {
if let Some(item) = instance.as_string() {
$validation_fn(&item)
} else {
true
}
}
fn validate<'i>(
&self,
instance: &F::Node<'i>,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if instance.is_string() {
if !Validate::<F>::is_valid(self, instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance.to_value(),
$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),
#[cfg(feature = "idna")]
(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, F: Json>(ctx: &compiler::Context<F>) -> CompilationResult<'a, F> {
let location = ctx.location().join("format");
let annotation = Arc::new(Value::String("regex".to_owned()));
Ok(Box::new(RegexValidator {
location,
annotation,
}))
}
}
impl<F: Json> Validate<F> for RegexValidator {
fn is_valid(&self, instance: &F::Node<'_>, ctx: &mut ValidationContext) -> bool {
if let Some(item) = instance.as_string() {
ctx.is_valid_ecma_regex(&item)
} else {
true
}
}
fn validate<'i>(
&self,
instance: &F::Node<'i>,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if instance.is_string() && !Validate::<F>::is_valid(self, instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance.to_value(),
"regex",
));
}
Ok(())
}
impl_format_evaluate!();
}
struct EmailValidator {
location: Location,
annotation: Arc<Value>,
email_options: Option<EmailAddressOptions>,
}
impl EmailValidator {
pub(crate) fn compile<'a, F: Json>(ctx: &compiler::Context<F>) -> CompilationResult<'a, F> {
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<F: Json> Validate<F> for EmailValidator {
fn is_valid(&self, instance: &F::Node<'_>, _ctx: &mut ValidationContext) -> bool {
if let Some(item) = instance.as_string() {
is_valid_email(&item, self.email_options.as_ref())
} else {
true
}
}
fn validate<'i>(
&self,
instance: &F::Node<'i>,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if instance.is_string() && !Validate::<F>::is_valid(self, instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance.to_value(),
"email",
));
}
Ok(())
}
impl_format_evaluate!();
}
#[cfg(feature = "idna")]
struct IdnEmailValidator {
location: Location,
annotation: Arc<Value>,
email_options: Option<EmailAddressOptions>,
}
#[cfg(feature = "idna")]
impl IdnEmailValidator {
pub(crate) fn compile<'a, F: Json>(ctx: &compiler::Context<F>) -> CompilationResult<'a, F> {
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,
}))
}
}
#[cfg(feature = "idna")]
impl<F: Json> Validate<F> for IdnEmailValidator {
fn is_valid(&self, instance: &F::Node<'_>, _ctx: &mut ValidationContext) -> bool {
if let Some(item) = instance.as_string() {
is_valid_idn_email(&item, self.email_options.as_ref())
} else {
true
}
}
fn validate<'i>(
&self,
instance: &F::Node<'i>,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if instance.is_string() && !Validate::<F>::is_valid(self, instance, ctx) {
return Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance.to_value(),
"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, F: Json>(
ctx: &compiler::Context<F>,
format_name: String,
check: Arc<dyn Format>,
) -> CompilationResult<'a, F> {
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<F: Json> Validate<F> for CustomFormatValidator {
fn validate<'i>(
&self,
instance: &F::Node<'i>,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
if Validate::<F>::is_valid(self, instance, ctx) {
Ok(())
} else {
Err(ValidationError::format(
self.location.clone(),
crate::paths::capture_evaluation_path(tracker, &self.location),
location.into(),
instance.to_value(),
self.format_name.clone(),
))
}
}
fn is_valid(&self, instance: &F::Node<'_>, _ctx: &mut ValidationContext) -> bool {
if let Some(item) = instance.as_string() {
self.check.is_valid(&item)
} else {
true
}
}
impl_format_evaluate!();
}
struct AnnotationOnlyFormatValidator {
annotation: Arc<Value>,
}
impl<F: Json> Validate<F> for AnnotationOnlyFormatValidator {
fn is_valid(&self, _instance: &F::Node<'_>, _ctx: &mut ValidationContext) -> bool {
true
}
fn validate<'i>(
&self,
_instance: &F::Node<'i>,
_location: &LazyLocation,
_tracker: Option<&RefTracker>,
_ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>> {
Ok(())
}
fn collect_errors<'i>(
&self,
_instance: &F::Node<'i>,
_location: &LazyLocation,
_tracker: Option<&RefTracker>,
_ctx: &mut ValidationContext,
_errors: &mut Vec<ValidationError<'i>>,
) {
}
fn evaluate(
&self,
instance: &F::Node<'_>,
_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)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, VariantArray)]
pub(crate) enum BuiltinFormat {
Date,
DateTime,
Duration,
Email,
Hostname,
HostnameDraft4,
#[cfg(feature = "idna")]
IdnEmail,
#[cfg(feature = "idna")]
IdnHostname,
Ipv4,
Ipv6,
Iri,
IriReference,
JsonPointer,
Regex,
RelativeJsonPointer,
Time,
Uri,
UriReference,
UriTemplate,
Uuid,
}
pub(crate) fn builtin_format(draft: Draft, format: &str) -> Option<BuiltinFormat> {
match format {
"date" => Some(BuiltinFormat::Date),
"date-time" => Some(BuiltinFormat::DateTime),
"duration" if draft >= Draft::Draft201909 => Some(BuiltinFormat::Duration),
"email" => Some(BuiltinFormat::Email),
"hostname" if matches!(draft, Draft::Draft4 | Draft::Draft6) => {
Some(BuiltinFormat::HostnameDraft4)
}
"hostname" => Some(BuiltinFormat::Hostname),
#[cfg(feature = "idna")]
"idn-email" => Some(BuiltinFormat::IdnEmail),
#[cfg(feature = "idna")]
"idn-hostname" if draft >= Draft::Draft7 => Some(BuiltinFormat::IdnHostname),
"ipv4" => Some(BuiltinFormat::Ipv4),
"ipv6" => Some(BuiltinFormat::Ipv6),
"iri" if draft >= Draft::Draft7 => Some(BuiltinFormat::Iri),
"iri-reference" if draft >= Draft::Draft7 => Some(BuiltinFormat::IriReference),
"json-pointer" if draft >= Draft::Draft6 => Some(BuiltinFormat::JsonPointer),
"regex" => Some(BuiltinFormat::Regex),
"relative-json-pointer" if draft >= Draft::Draft7 => {
Some(BuiltinFormat::RelativeJsonPointer)
}
"time" => Some(BuiltinFormat::Time),
"uri" => Some(BuiltinFormat::Uri),
"uri-reference" if draft >= Draft::Draft6 => Some(BuiltinFormat::UriReference),
"uri-template" if draft >= Draft::Draft6 => Some(BuiltinFormat::UriTemplate),
"uuid" if draft >= Draft::Draft201909 => Some(BuiltinFormat::Uuid),
_ => None,
}
}
impl BuiltinFormat {
#[must_use]
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Date => "date",
Self::DateTime => "date-time",
Self::Duration => "duration",
Self::Email => "email",
Self::Hostname | Self::HostnameDraft4 => "hostname",
#[cfg(feature = "idna")]
Self::IdnEmail => "idn-email",
#[cfg(feature = "idna")]
Self::IdnHostname => "idn-hostname",
Self::Ipv4 => "ipv4",
Self::Ipv6 => "ipv6",
Self::Iri => "iri",
Self::IriReference => "iri-reference",
Self::JsonPointer => "json-pointer",
Self::Regex => "regex",
Self::RelativeJsonPointer => "relative-json-pointer",
Self::Time => "time",
Self::Uri => "uri",
Self::UriReference => "uri-reference",
Self::UriTemplate => "uri-template",
Self::Uuid => "uuid",
}
}
#[must_use]
pub(crate) const fn length_window(self) -> Option<(u64, u64)> {
match self {
Self::Date => Some((10, 10)),
Self::DateTime => Some((20, u64::MAX)),
Self::Time => Some((9, u64::MAX)),
Self::Duration => Some((3, u64::MAX)),
Self::Hostname | Self::HostnameDraft4 => Some((1, 253)),
Self::Uuid => Some((36, 36)),
Self::Ipv4 => Some((7, 15)),
Self::Ipv6 => Some((2, 45)),
#[cfg(feature = "idna")]
Self::IdnEmail | Self::IdnHostname => None,
Self::Email
| Self::Iri
| Self::IriReference
| Self::JsonPointer
| Self::Regex
| Self::RelativeJsonPointer
| Self::Uri
| Self::UriReference
| Self::UriTemplate => None,
}
}
#[must_use]
pub(crate) const fn witness(self) -> &'static str {
match self {
Self::Date => "2020-01-01",
Self::DateTime => "2020-01-01T00:00:00Z",
Self::Duration => "P1D",
Self::Email => "a@b.co",
#[cfg(feature = "idna")]
Self::IdnEmail => "a@b.co",
Self::Hostname | Self::HostnameDraft4 => "example.com",
#[cfg(feature = "idna")]
Self::IdnHostname => "example.com",
Self::Ipv4 => "127.0.0.1",
Self::Ipv6 => "::1",
Self::Iri | Self::IriReference | Self::Uri | Self::UriReference => "http://example.com",
Self::JsonPointer => "/a",
Self::Regex => "a",
Self::RelativeJsonPointer => "0",
Self::Time => "00:00:00Z",
Self::UriTemplate => "http://example.com/{id}",
Self::Uuid => "00000000-0000-4000-8000-000000000000",
}
}
pub(crate) fn is_valid(self, text: &str) -> bool {
match self {
Self::Date => is_valid_date(text),
Self::DateTime => is_valid_datetime(text),
Self::Duration => is_valid_duration(text),
Self::Email => is_valid_email(text, None),
Self::Hostname => is_valid_hostname(text),
Self::HostnameDraft4 => is_valid_hostname_rfc1034(text),
#[cfg(feature = "idna")]
Self::IdnEmail => is_valid_idn_email(text, None),
#[cfg(feature = "idna")]
Self::IdnHostname => is_valid_idn_hostname(text),
Self::Ipv4 => is_valid_ipv4(text),
Self::Ipv6 => is_valid_ipv6(text),
Self::Iri => is_valid_iri(text),
Self::IriReference => is_valid_iri_reference(text),
Self::JsonPointer => is_valid_json_pointer(text),
Self::Regex => is_valid_regex(text),
Self::RelativeJsonPointer => is_valid_relative_json_pointer(text),
Self::Time => is_valid_time(text),
Self::Uri => is_valid_uri(text),
Self::UriReference => is_valid_uri_reference(text),
Self::UriTemplate => is_valid_uri_template(text),
Self::Uuid => is_valid_uuid(text),
}
}
}
#[inline]
pub(crate) fn compile<'a, F: Json>(
ctx: &compiler::Context<F>,
_: &'a Map<String, Value>,
schema: &'a Value,
) -> Option<CompilationResult<'a, F>> {
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 builtin_format(draft, format) {
Some(BuiltinFormat::Date) => Some(DateValidator::compile(ctx)),
Some(BuiltinFormat::DateTime) => Some(DateTimeValidator::compile(ctx)),
Some(BuiltinFormat::Duration) => Some(DurationValidator::compile(ctx)),
Some(BuiltinFormat::Email) => Some(EmailValidator::compile(ctx)),
Some(BuiltinFormat::Hostname) => Some(HostnameValidator::compile(ctx)),
Some(BuiltinFormat::HostnameDraft4) => Some(HostnameValidatorDraft4::compile(ctx)),
#[cfg(feature = "idna")]
Some(BuiltinFormat::IdnEmail) => Some(IdnEmailValidator::compile(ctx)),
#[cfg(feature = "idna")]
Some(BuiltinFormat::IdnHostname) => Some(IdnHostnameValidator::compile(ctx)),
Some(BuiltinFormat::Ipv4) => Some(IpV4Validator::compile(ctx)),
Some(BuiltinFormat::Ipv6) => Some(IpV6Validator::compile(ctx)),
Some(BuiltinFormat::Iri) => Some(IriValidator::compile(ctx)),
Some(BuiltinFormat::IriReference) => Some(IriReferenceValidator::compile(ctx)),
Some(BuiltinFormat::JsonPointer) => Some(JsonPointerValidator::compile(ctx)),
Some(BuiltinFormat::Regex) => Some(RegexValidator::compile(ctx)),
Some(BuiltinFormat::RelativeJsonPointer) => {
Some(RelativeJsonPointerValidator::compile(ctx))
}
Some(BuiltinFormat::Time) => Some(TimeValidator::compile(ctx)),
Some(BuiltinFormat::Uri) => Some(UriValidator::compile(ctx)),
Some(BuiltinFormat::UriReference) => Some(UriReferenceValidator::compile(ctx)),
Some(BuiltinFormat::UriTemplate) => Some(UriTemplateValidator::compile(ctx)),
Some(BuiltinFormat::Uuid) => Some(UuidValidator::compile(ctx)),
None => {
if ctx.are_unknown_formats_ignored() {
None
} else {
let message = if ctx.asserts_formats_by_dialect() {
format!(
"Unknown format: '{format}'. The meta-schema asserts formats, so unrecognized ones cannot be ignored. Register a check for it or disable format validation"
)
} else {
format!(
"Unknown format: '{format}'. Adjust configuration to ignore unrecognized formats"
)
};
let location = ctx.location().join("format");
Some(Err(ValidationError::compile_error(
location.clone(),
location,
Location::new(),
Cow::Borrowed(schema),
message,
)))
}
}
}
} 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(),
Cow::Borrowed(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_case("egbpdaj6bu4bxfgehfvwxn" => Some("ليهمابتكلموشعربي؟".to_string()); "rfc a arabic")]
#[test_case("ihqwcrb4cv8a8dqg056pqjye" => Some("他们为什么不说中文".to_string()); "rfc b chinese simplified")]
#[test_case("ihqwctvzc91f659drss3x8bo0yb" => Some("他們爲什麽不說中文".to_string()); "rfc c chinese traditional")]
#[test_case("Proprostnemluvesky-uyb24dma41a" => Some("Pročprostěnemluvíčesky".to_string()); "rfc d czech")]
#[test_case("4dbcagdahymbxekheh6e0a7fei0b" => Some("למההםפשוטלאמדבריםעברית".to_string()); "rfc e hebrew")]
#[test_case("i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd" => Some("यहलोगहिन्दीक्योंनहींबोलसकतेहैं".to_string()); "rfc f hindi")]
#[test_case("n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa" => Some("なぜみんな日本語を話してくれないのか".to_string()); "rfc g japanese")]
#[test_case("989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5jpsd879ccm6fea98c" => Some("세계의모든사람들이한국어를이해한다면얼마나좋을까".to_string()); "rfc h korean")]
#[test_case("b1abfaaepdrnnbgefbaDotcwatmq2g4l" => Some("почемужеонинеговорятпорусски".to_string()); "rfc i russian")]
#[test_case("PorqunopuedensimplementehablarenEspaol-fmd56a" => Some("PorquénopuedensimplementehablarenEspañol".to_string()); "rfc j spanish")]
#[test_case("TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g" => Some("TạisaohọkhôngthểchỉnóitiếngViệt".to_string()); "rfc k vietnamese")]
#[test_case("3B-ww4c5e180e575a65lsy2b" => Some("3年B組金八先生".to_string()); "rfc l")]
#[test_case("-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n" => Some("安室奈美恵-with-SUPER-MONKEYS".to_string()); "rfc m leading hyphen in basic part")]
#[test_case("Hello-Another-Way--fc4qua05auwb3674vfr0b" => Some("Hello-Another-Way-それぞれの場所".to_string()); "rfc n double hyphen")]
#[test_case("2-u9tlzr9756bt3uc0v" => Some("ひとつ屋根の下2".to_string()); "rfc o")]
#[test_case("MajiKoi5-783gue6qz075azm5e" => Some("MajiでKoiする5秒前".to_string()); "rfc p")]
#[test_case("de-jg4avhby1noc0d" => Some("パフィーdeルンバ".to_string()); "rfc q")]
#[test_case("d9juau41awczczp" => Some("そのスピードで".to_string()); "rfc r")]
#[test_case("-> $1.00 <--" => Some("-> $1.00 <-".to_string()); "rfc s ascii only")]
#[test_case("abc-" => Some("abc".to_string()); "empty extended part")]
#[test_case("" => Some(String::new()); "empty")]
#[test_case("a" => Some("\u{80}".to_string()); "no basic code points")]
#[test_case("99999999999999999999" => None; "overflow")]
#[test_case("-t7g" => None; "leading delimiter is not a separator")]
#[test_case("é" => None; "non-ascii")]
#[test_case("ss" => None; "incomplete")]
fn test_decode_punycode(input: &str) -> Option<String> {
decode_punycode(input)
}
#[cfg(feature = "idna")]
#[test]
#[allow(clippy::cast_possible_truncation)]
fn differential_against_idna() {
let alphabet: Vec<u8> = (0x20u8..=0x7e).collect();
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let mut checked = 0u32;
for _ in 0..400_000 {
let len = (next() % 40) as usize;
let candidate: String = (0..len)
.map(|_| char::from(alphabet[(next() % alphabet.len() as u64) as usize]))
.collect();
let ours = decode_punycode(&candidate);
let theirs = idna::punycode::decode_to_string(&candidate);
assert_eq!(ours, theirs, "diverged on {candidate:?}");
checked += 1;
}
assert_eq!(checked, 400_000);
}
#[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")]
#[test_case("P1WT1H")]
fn test_invalid_duration(input: &str) {
assert!(!is_valid_duration(input));
}
#[cfg(not(feature = "idna"))]
#[test_case("idn-hostname")]
#[test_case("idn-email")]
fn idn_formats_are_unknown_without_idna(format: &str) {
let schema = json!({"format": format, "type": "string"});
let validator = crate::options()
.should_validate_formats(true)
.build(&schema)
.expect("a valid schema");
assert!(validator.is_valid(&json!("anything")));
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(),
format!(
"Unknown format: '{format}'. Adjust configuration to ignore unrecognized formats"
)
);
}
#[cfg(feature = "idna")]
#[test_case(
"idn-hostname",
"\u{5B9F}\u{4F8B}.\u{30C6}\u{30B9}\u{30C8}",
"-\u{5B9F}\u{4F8B}"
)]
#[test_case("idn-email", "\u{03B1}@\u{03C0}\u{03B1}\u{03C1}.gr", "a@-.gr")]
fn idn_formats_validate_with_idna(format: &str, accepted: &str, rejected: &str) {
let schema = json!({"format": format, "type": "string"});
let validator = crate::options()
.should_validate_formats(true)
.build(&schema)
.expect("a valid schema");
assert!(validator.is_valid(&json!(accepted)));
assert!(!validator.is_valid(&json!(rejected)));
}
#[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() {
let _ = is_valid_datetime("2624-04-25t23:14:04-256\x112");
}
#[cfg(feature = "idna")]
#[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));
}
#[cfg(feature = "idna")]
#[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")));
}
#[cfg(feature = "idna")]
#[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")));
}
#[cfg(feature = "idna")]
#[test_case("δοκιμή@example.com", true; "non-ASCII local part")]
#[test_case("\"δοκιμή\"@example.com", true; "non-ASCII quoted local part")]
#[test_case("user@example.com", true; "ascii local part")]
#[test_case("not-an-email", false; "no domain")]
#[test_case("δοκιμή@-bad-.com", false; "non-ASCII local part with invalid domain")]
fn idn_email_non_ascii(input: &str, expected: bool) {
let validator = crate::options()
.should_validate_formats(true)
.build(&json!({"format": "idn-email", "type": "string"}))
.expect("Schema should compile");
assert_eq!(validator.is_valid(&json!(input)), expected);
}
#[test_case("\"δοκιμή\"@example.com", false; "non-ASCII quoted local part")]
#[test_case("user@example.com", true; "ascii local part")]
fn email_ascii_only_local_part(input: &str, expected: bool) {
let validator = crate::options()
.should_validate_formats(true)
.build(&json!({"format": "email", "type": "string"}))
.expect("Schema should compile");
assert_eq!(validator.is_valid(&json!(input)), expected);
}
#[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")]
#[test_case("a'b"; "apostrophe in literal")]
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));
}
#[cfg(feature = "macros")]
#[test]
fn is_valid_regex_cache_matches_fresh_parse() {
let mut patterns: Vec<String> = (0..20).map(|i| format!("^val{i}[a-z]+$")).collect();
patterns.extend((0..20).map(|i| format!("[unclosed{i}")));
for _ in 0..3 {
for pattern in &patterns {
assert_eq!(
is_valid_regex(pattern),
jsonschema_regex::to_rust_regex(pattern).is_ok(),
"pattern {pattern}"
);
}
}
}
#[test]
fn every_builtin_format_witness_satisfies_its_own_format() {
for format in BuiltinFormat::VARIANTS {
assert!(
format.is_valid(format.witness()),
"{format:?} witness `{}` does not satisfy `{}`",
format.witness(),
format.as_str()
);
if let Some((minimum, maximum)) = format.length_window() {
let length = format.witness().chars().count() as u64;
assert!(
(minimum..=maximum).contains(&length),
"{format:?} witness `{}` falls outside its declared length window",
format.witness()
);
}
}
}
#[test]
fn the_longest_hostname_is_accepted() {
let label = "a".repeat(63);
let hostname = format!("{label}.{label}.{label}.{}", "a".repeat(61));
assert_eq!(hostname.len(), 253);
assert!(is_valid_hostname(&hostname));
assert_eq!(BuiltinFormat::Hostname.length_window(), Some((1, 253)));
}
#[test_case("00:00:00Z", BuiltinFormat::Time; "shortest time")]
#[test_case("0000-01-01T00:00:00Z", BuiltinFormat::DateTime; "shortest date-time")]
#[test_case("P1D", BuiltinFormat::Duration; "shortest duration")]
#[test_case("::", BuiltinFormat::Ipv6; "shortest ipv6")]
#[test_case(
"0000:0000:0000:0000:0000:0000:255.255.255.255",
BuiltinFormat::Ipv6;
"longest ipv6"
)]
#[test_case("a", BuiltinFormat::Hostname; "shortest hostname")]
fn test_window_end_is_accepted(text: &str, format: BuiltinFormat) {
assert!(format.is_valid(text), "`{text}` is not a valid {format:?}");
let (minimum, maximum) = format.length_window().expect("has a window");
let length = text.chars().count() as u64;
assert!(
length == minimum || length == maximum,
"`{text}` sits inside the window rather than at an end"
);
}
}