use super::Finder;
use std::ops::Range;
const SUB_DELIMS_STRICT: [bool; 256] = {
let mut table = [false; 256];
table[b'!' as usize] = true;
table[b'$' as usize] = true;
table[b'&' as usize] = true;
table[b'\'' as usize] = true;
table[b'(' as usize] = true;
table[b')' as usize] = true;
table[b'*' as usize] = true;
table[b'+' as usize] = true;
table[b',' as usize] = true;
table[b';' as usize] = true;
table[b'=' as usize] = true;
table
};
const SUB_DELIMS_LAX: [bool; 256] = {
let mut table = [false; 256];
table[b'!' as usize] = true;
table[b'$' as usize] = true;
table[b'&' as usize] = true;
table[b'(' as usize] = true;
table[b'*' as usize] = true;
table[b'+' as usize] = true;
table[b',' as usize] = true;
table[b';' as usize] = true;
table[b'=' as usize] = true;
table
};
const PCHAR: u8 = 1 << 0;
const QUERY: u8 = 1 << 1;
const USERINFO: u8 = 1 << 2;
const LABEL: u8 = 1 << 3;
const fn body_classes(strict: bool) -> [u8; 256] {
let mut table = [0u8; 256];
let mut b = 0usize;
while b < 256 {
let c = b as u8;
let unreserved =
c.is_ascii_alphanumeric() || c == b'-' || c == b'.' || c == b'_' || c == b'~';
let sub_delim = if strict {
SUB_DELIMS_STRICT[b]
} else {
SUB_DELIMS_LAX[b]
};
let mut flags = 0u8;
if unreserved || sub_delim || c == b':' {
flags |= USERINFO;
}
if unreserved || sub_delim || c == b':' || c == b'@' || (!strict && c == b')') {
flags |= PCHAR;
}
if flags & PCHAR != 0 || c == b'/' || c == b'?' {
flags |= QUERY;
}
if c.is_ascii_alphanumeric() || c == b'_' || c == b'-' {
flags |= LABEL;
}
table[b] = flags;
b += 1;
}
table
}
static BODY_CLASSES_STRICT: [u8; 256] = body_classes(true);
static BODY_CLASSES_LAX: [u8; 256] = body_classes(false);
const MAX_SCHEME_LEN: usize = 64;
#[derive(Default, Clone, Copy)]
struct SchemeConfig(u8);
impl SchemeConfig {
fn has(&self, flag: u8) -> bool {
(self.0 & flag) != 0
}
}
fn scheme_config(scheme: &str) -> SchemeConfig {
if ["ftp", "http", "https"]
.iter()
.any(|s| scheme.eq_ignore_ascii_case(s))
{
SchemeConfig(DISALLOW_EMPTY_HOST)
} else {
SchemeConfig::default()
}
}
fn can_end_registered_scheme(prev2: u8, prev1: u8) -> bool {
static TABLE: std::sync::OnceLock<Box<[u64; 1024]>> = std::sync::OnceLock::new();
let table = TABLE.get_or_init(|| {
let mut table = Box::new([0u64; 1024]);
let mut set = |a: u8, b: u8| {
let index = usize::from(a) << 8 | usize::from(b);
table[index >> 6] |= 1 << (index & 63);
};
for scheme in crate::iana::URI_SCHEMES.iter() {
let bytes = scheme.as_bytes();
match bytes {
[] => {}
[only] => {
for a in 0..=255u8 {
set(a, *only);
}
}
[.., a, b] => set(*a, *b),
}
}
table
});
let index =
usize::from(prev2.to_ascii_lowercase()) << 8 | usize::from(prev1.to_ascii_lowercase());
table[index >> 6] & (1 << (index & 63)) != 0
}
fn is_registered_scheme(scheme: &str) -> bool {
crate::registry::uri_schemes().contains(scheme.as_bytes())
}
const DISALLOW_EMPTY_HOST: u8 = 1 << 0;
#[derive(Default)]
pub struct URI {
schemes: Vec<String>,
scheme_ends: [u64; 4],
pub strict: bool,
}
impl Finder for URI {
fn id(&self) -> &'static str {
"uri"
}
fn triggerable(&self) -> bool {
true
}
fn line_agnostic(&self) -> bool {
true
}
fn could_trigger_at(&self, byte: u8) -> bool {
byte == b':'
}
fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
Self::is_alpha(prev) || Self::is_digit(prev) || matches!(prev, b'+' | b'-' | b'.')
}
fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
if self.strict {
return true;
}
next.is_ascii_alphanumeric()
|| matches!(
next,
b'-' | b'.'
| b'_'
| b'~'
| b'%'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b'@'
| b'/'
| b'?'
| b'#'
)
}
fn has_trigger_context(&self) -> bool {
!self.strict || !self.schemes.is_empty()
}
fn trigger_context(&self, prev2: u8, prev1: u8) -> bool {
if !self.schemes.is_empty() {
let (prev2, prev1) = (prev2.to_ascii_lowercase(), prev1.to_ascii_lowercase());
if self.scheme_ends[usize::from(prev1 >> 6)] & (1 << (prev1 & 63)) == 0 {
return false;
}
return self.schemes.iter().any(|s| match s.as_bytes() {
[] => false,
[only] => *only == prev1,
[.., a, b] => *a == prev2 && *b == prev1,
});
}
self.strict || can_end_registered_scheme(prev2, prev1)
}
fn trigger_context_exempt(&self, next: Option<u8>) -> bool {
self.schemes.is_empty() && next == Some(b'/')
}
fn try_trigger_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
self.try_at_colon(input, pos)
}
fn find(&self, s: &str) -> Option<Range<usize>> {
let input = s.as_bytes();
let mut idx = 0;
while idx < input.len() {
let colon_idx = idx + input[idx..].iter().position(|&b| b == b':')?;
idx = colon_idx + 1;
if let Some(range) = self.try_at_colon(input, colon_idx) {
return Some(range);
}
}
None
}
}
impl URI {
pub fn add_scheme(&mut self, s: &str) {
let lower = s.to_lowercase();
if let Some(&last) = lower.as_bytes().last() {
self.scheme_ends[usize::from(last >> 6)] |= 1 << (last & 63);
}
if let Err(pos) = self.schemes.binary_search(&lower) {
self.schemes.insert(pos, lower);
}
}
fn try_at_colon(&self, input: &[u8], colon_idx: usize) -> Option<Range<usize>> {
if input[colon_idx] != b':' {
return None;
}
if !self.strict
&& self.schemes.is_empty()
&& input.get(colon_idx + 1) != Some(&b'/')
&& colon_idx >= 1
{
let prev1 = input[colon_idx - 1];
let prev2 = if colon_idx >= 2 {
input[colon_idx - 2]
} else {
b' '
};
if !can_end_registered_scheme(prev2, prev1) {
return None;
}
}
let scheme_idx = self.rlook_scheme(&input[..colon_idx])?;
let scheme = std::str::from_utf8(&input[scheme_idx..colon_idx]).ok()?;
if !self.schemes.is_empty() && !self.schemes.iter().any(|s| s.eq_ignore_ascii_case(scheme))
{
return None;
}
if !self.strict
&& self.schemes.is_empty()
&& !input[colon_idx + 1..].starts_with(b"//")
&& !is_registered_scheme(scheme)
{
return None;
}
let scheme_config = scheme_config(scheme);
if !self.strict && input.get(colon_idx + 1) == Some(&b':') {
return None;
}
let mut idx = colon_idx + 1;
let hier_len = self.look_hier_part(&input[idx..], scheme_config)?;
idx += hier_len;
idx += self.look_question_mark_query(&input[idx..]).unwrap_or(0);
idx += self.look_sharp_fragment(&input[idx..]).unwrap_or(0);
if !self.strict {
idx = Self::trim_lax(input, colon_idx, idx);
let tail = &input[colon_idx + 1..idx];
if tail
.iter()
.all(|&b| matches!(b, b'.' | b',' | b';' | b':' | b'!' | b'?' | b'#'))
{
return None;
}
if hier_len == 0 && scheme_idx > 0 && !Self::is_prose_delimiter(input[scheme_idx - 1]) {
return None;
}
if self.schemes.is_empty() && !tail.starts_with(b"//") && !is_registered_scheme(scheme)
{
return None;
}
}
Some(scheme_idx..idx)
}
fn trim_lax(input: &[u8], colon_idx: usize, mut end: usize) -> usize {
let body_start = colon_idx + 1;
if memchr::memchr(b')', &input[body_start..end]).is_some() {
let mut depth = 0usize;
for (i, &b) in input[body_start..end].iter().enumerate() {
match b {
b'(' => depth += 1,
b')' => {
if depth == 0 {
end = body_start + i;
break;
}
depth -= 1;
}
_ => {}
}
}
}
while end > body_start && matches!(input[end - 1], b'.' | b',' | b';' | b':' | b'!' | b'?')
{
end -= 1;
}
end
}
#[inline]
fn is_prose_delimiter(b: u8) -> bool {
b <= b' '
|| b >= 0x80
|| matches!(
b,
b'"' | b'\'' | b'<' | b'>' | b'\\' | b'^' | b'`' | b'{' | b'|' | b'}' | b'(' | b'['
)
}
fn rlook_scheme(&self, input: &[u8]) -> Option<usize> {
let start = input.len().saturating_sub(MAX_SCHEME_LEN);
let mut idx = None;
let mut run_start = input.len();
for (i, &c) in input[start..].iter().enumerate().rev() {
if Self::is_alpha(c) {
idx = Some(start + i);
run_start = start + i;
} else if Self::is_digit(c) || c == b'+' || c == b'-' || c == b'.' {
run_start = start + i;
} else {
break;
}
}
if run_start < input.len() && Self::is_digit(input[run_start]) {
return None;
}
idx
}
fn look_hier_part(&self, input: &[u8], sc: SchemeConfig) -> Option<usize> {
if let Some(idx) = self.look_slash_slash(input) {
let idx = idx + self.look_authority(&input[idx..], sc)?;
return Some(idx + self.look_path_abempty(&input[idx..]));
}
if sc.has(DISALLOW_EMPTY_HOST) {
return None;
}
if let Some(idx) = self.look_slash(input).map(|idx| {
idx + self
.look_segment_nz(&input[idx..])
.map(|i| i + self.look_path_abempty(&input[idx + i..]))
.unwrap_or(0)
}) {
return Some(idx);
}
if let Some(idx) = self
.look_segment_nz(input)
.map(|idx| idx + self.look_path_abempty(&input[idx..]))
{
return Some(idx);
}
Some(0)
}
fn look_authority(&self, input: &[u8], sc: SchemeConfig) -> Option<usize> {
let mut idx = 0;
idx += self.look_userinfo_at(&input[idx..]).unwrap_or(0);
idx += self
.look_host(&input[idx..])
.filter(|&i| !(i == 0 && sc.has(DISALLOW_EMPTY_HOST)))?;
if let Some(i) = self.look_colon(&input[idx..]) {
idx += i;
let port_len = self.look_port(&input[idx..]);
idx += port_len;
let glued = input.get(idx).is_some_and(|&b| {
b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || (b == b'.' && port_len > 0)
});
if glued {
return None;
}
}
Some(idx)
}
fn look_path_abempty(&self, input: &[u8]) -> usize {
let mut idx = 0;
while idx < input.len() {
idx += match self
.look_slash(&input[idx..])
.map(|i| i + self.look_segment(&input[idx + i..]))
{
Some(n) => n,
None => break,
};
}
idx
}
fn look_segment(&self, input: &[u8]) -> usize {
self.look_run(input, PCHAR)
}
#[inline]
fn look_run(&self, input: &[u8], class: u8) -> usize {
let table = self.body_classes();
let mut idx = 0;
while idx < input.len() {
let b = input[idx];
if table[b as usize] & class != 0 {
idx += 1;
} else if b == b'%' {
match self.look_pct_encoded(&input[idx..]) {
Some(n) => idx += n,
None => break,
}
} else {
break;
}
}
idx
}
#[inline]
fn body_classes(&self) -> &'static [u8; 256] {
if self.strict {
&BODY_CLASSES_STRICT
} else {
&BODY_CLASSES_LAX
}
}
fn look_segment_nz(&self, input: &[u8]) -> Option<usize> {
match self.look_segment(input) {
0 => None,
n => Some(n),
}
}
fn look_userinfo_at(&self, input: &[u8]) -> Option<usize> {
let table = self.body_classes();
let mut idx = 0;
while idx < input.len() && idx < 256 {
let b = input[idx];
if b == b'@' {
return Some(idx + 1);
}
if table[b as usize] & USERINFO != 0 {
idx += 1;
} else if b == b'%' {
idx += self.look_pct_encoded(&input[idx..])?;
} else {
return None;
}
}
None
}
fn look_host(&self, input: &[u8]) -> Option<usize> {
if let Some(idx) = self.look_ip_literal(input) {
if input
.get(idx)
.is_some_and(|&b| b.is_ascii_alphanumeric() || b == b'.' || b == b'+' || b == b'-')
{
return None;
}
return Some(idx);
}
if let Some(idx) = self.look_ipv4_address(input) {
match input.get(idx) {
Some(&b) if b == b'.' || b == b'-' || b == b'_' || b.is_ascii_alphanumeric() => {}
Some(&b) if b >= 0x80 => return None,
_ => return Some(idx),
}
}
self.look_hostname(input)
}
fn look_ip_literal(&self, input: &[u8]) -> Option<usize> {
let mut idx = 0;
idx += self.look_left_bracket(&input[idx..])?;
let right_bracket_index = input[idx..].iter().take(64).position(|&b| b == b']')?;
if right_bracket_index > 0 {
let end = idx + right_bracket_index;
let slice = &input[idx..end];
if self.is_ipv6address(slice) || self.is_ipv6addrz(slice) || self.is_ipvfuture(slice) {
return Some(end + 1);
}
}
None
}
fn is_ipv6addrz(&self, input: &[u8]) -> bool {
let Some(pct) = input.iter().position(|&b| b == b'%') else {
return false;
};
if !self.is_ipv6address(&input[..pct]) {
return false;
}
let mut zone = &input[pct + 1..];
if zone.starts_with(b"25") && zone.len() > 2 {
zone = &zone[2..]; }
!zone.is_empty() && zone.iter().all(|&b| Self::is_unreserved(b))
}
fn is_ipv6address(&self, input: &[u8]) -> bool {
crate::ipv6::is_valid_ipv6(input)
}
fn is_ipvfuture(&self, input: &[u8]) -> bool {
if input.is_empty() || input[0] != b'v' {
return false;
}
let mut idx = 1;
let hexdig_start = idx;
while idx < input.len() && Self::is_hexdig(input[idx]) {
idx += 1;
}
if idx == hexdig_start {
return false; }
if idx >= input.len() || input[idx] != b'.' {
return false;
}
idx += 1;
let char_start = idx;
while idx < input.len() {
let c = input[idx];
if Self::is_unreserved(c) || Self::is_sub_delim_strict(c) || c == b':' {
idx += 1;
} else {
break;
}
}
if idx == char_start {
return false; }
idx == input.len()
}
#[inline]
fn is_sub_delim_strict(c: u8) -> bool {
SUB_DELIMS_STRICT[c as usize]
}
fn look_ipv4_address(&self, input: &[u8]) -> Option<usize> {
let mut idx = 0;
idx += self.look_dec_octet(&input[idx..])?;
idx += self.look_period(&input[idx..])?;
idx += self.look_dec_octet(&input[idx..])?;
idx += self.look_period(&input[idx..])?;
idx += self.look_dec_octet(&input[idx..])?;
idx += self.look_period(&input[idx..])?;
idx += self.look_dec_octet(&input[idx..])?;
Some(idx)
}
fn look_dec_octet(&self, input: &[u8]) -> Option<usize> {
if input.len() >= 3
&& input[0] == b'2'
&& input[1] == b'5'
&& Self::is_digit_0_to_5(input[2])
{
return Some(3);
}
if input.len() >= 3
&& input[0] == b'2'
&& Self::is_digit_0_to_4(input[1])
&& Self::is_digit(input[2])
{
return Some(3);
}
if input.len() >= 3
&& input[0] == b'1'
&& Self::is_digit(input[1])
&& Self::is_digit(input[2])
{
return Some(3);
}
if input.len() >= 2 && Self::is_digit_1_to_9(input[0]) && Self::is_digit(input[1]) {
return Some(2);
}
if !input.is_empty() && Self::is_digit(input[0]) {
return Some(1);
}
None
}
fn look_hostname(&self, input: &[u8]) -> Option<usize> {
const MAX_LABEL: usize = 63; const MAX_HOSTNAME: usize = 253;
let mut idx = 0;
loop {
let run = Self::label_run_len(&input[idx..]);
if run == 0 {
break; }
if run > MAX_LABEL {
return None;
}
idx += run;
if input.get(idx) == Some(&b'.') && Self::label_run_len(&input[idx + 1..]) > 0 {
idx += 1;
continue;
}
break;
}
if idx > MAX_HOSTNAME {
return None;
}
match input.get(idx) {
Some(&b) if b >= 0x80 => return None,
Some(&b'.') if input.get(idx + 1).is_some_and(|&b| b >= 0x80) => return None,
_ => {}
}
Some(idx)
}
fn label_run_len(input: &[u8]) -> usize {
let starts = input
.first()
.is_some_and(|&b| b.is_ascii_alphanumeric() || b == b'_');
if !starts {
return 0;
}
let table = &BODY_CLASSES_LAX;
let mut len = 1;
while len < input.len() && table[input[len] as usize] & LABEL != 0 {
len += 1;
}
len
}
fn look_port(&self, input: &[u8]) -> usize {
input.iter().take_while(|&&c| Self::is_digit(c)).count()
}
fn look_question_mark_query(&self, input: &[u8]) -> Option<usize> {
let mut idx = 0;
idx += self.look_question_mark(&input[idx..])?;
idx += self.look_query(&input[idx..]);
Some(idx)
}
fn look_query(&self, input: &[u8]) -> usize {
self.look_run(input, QUERY)
}
fn look_sharp_fragment(&self, input: &[u8]) -> Option<usize> {
let mut idx = 0;
idx += self.look_sharp(&input[idx..])?;
idx += self.look_fragment(&input[idx..]);
Some(idx)
}
fn look_fragment(&self, input: &[u8]) -> usize {
self.look_run(input, QUERY)
}
#[inline]
fn look_pct_encoded(&self, input: &[u8]) -> Option<usize> {
if input.len() >= 3
&& input[0] == b'%'
&& Self::is_hexdig(input[1])
&& Self::is_hexdig(input[2])
{
Some(3)
} else {
None
}
}
#[inline]
fn look_period(&self, input: &[u8]) -> Option<usize> {
if !input.is_empty() && input[0] == b'.' {
Some(1)
} else {
None
}
}
#[inline]
fn look_left_bracket(&self, input: &[u8]) -> Option<usize> {
if !input.is_empty() && input[0] == b'[' {
Some(1)
} else {
None
}
}
#[inline]
fn look_colon(&self, input: &[u8]) -> Option<usize> {
if !input.is_empty() && input[0] == b':' {
Some(1)
} else {
None
}
}
#[inline]
fn look_question_mark(&self, input: &[u8]) -> Option<usize> {
if !input.is_empty() && input[0] == b'?' {
Some(1)
} else {
None
}
}
#[inline]
fn look_sharp(&self, input: &[u8]) -> Option<usize> {
if !input.is_empty() && input[0] == b'#' {
Some(1)
} else {
None
}
}
#[inline]
fn look_slash(&self, input: &[u8]) -> Option<usize> {
if !input.is_empty() && input[0] == b'/' {
Some(1)
} else {
None
}
}
#[inline]
fn look_slash_slash(&self, input: &[u8]) -> Option<usize> {
if input.len() >= 2 && input[0] == b'/' && input[1] == b'/' {
Some(2)
} else {
None
}
}
#[inline]
fn is_unreserved(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'-' || c == b'.' || c == b'_' || c == b'~'
}
#[inline]
fn is_alpha(c: u8) -> bool {
c.is_ascii_alphabetic()
}
#[inline]
fn is_digit(c: u8) -> bool {
c.is_ascii_digit()
}
#[inline]
fn is_digit_1_to_9(c: u8) -> bool {
(b'1'..=b'9').contains(&c)
}
#[inline]
fn is_digit_0_to_4(c: u8) -> bool {
(b'0'..=b'4').contains(&c)
}
#[inline]
fn is_digit_0_to_5(c: u8) -> bool {
(b'0'..=b'5').contains(&c)
}
#[inline]
fn is_hexdig(c: u8) -> bool {
c.is_ascii_hexdigit()
}
}
#[cfg(test)]
#[allow(clippy::bool_assert_comparison)]
#[allow(clippy::useless_vec)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
#[test]
fn is_ipv6address_should_identify_valid_ipv6s() {
let finder = URI::default();
for input in [
"::",
"::1",
"1::",
"1:2:3:4:5:6:7:8",
"1:2:3:4:5:6::7",
"1:2:3:4:5:6:127.0.0.1",
"1::127.0.0.1",
] {
assert_eq!(true, finder.is_ipv6address(input.as_bytes()), "{}", input);
}
}
#[test]
fn is_ipv6address_should_identify_invalid_ipv6s() {
let finder = URI::default();
for input in vec![
" ",
" ::",
":: ",
" :: ",
":::",
"::1::",
":1:",
"1:2:3:4:5:6:7:8:9",
"1:2:3:4:5:6:7:127.0.0.1",
"1:2:3:4:5:6::7:8",
"1:2:3:4:5:6::127.0.0.1",
"1:127.0.0.1::",
] {
assert_eq!(false, finder.is_ipv6address(input.as_bytes()), "{}", input);
}
}
#[test]
fn look_path_abempty_should_mirror_the_len_of_valid_inputs() {
let finder = URI::default();
for input in vec![
"",
"/",
"//",
"///",
"/foo/bar",
"/rfc/rfc1808.txt",
"/with/trailing/",
] {
assert_eq!(
input.len(),
finder.look_path_abempty(input.as_bytes()),
"{}",
input
);
}
}
#[test]
fn look_path_abempty_should_skip_invalid_inputs() {
let finder = URI::default();
for input in vec!["foobar"] {
assert_eq!(0, finder.look_path_abempty(input.as_bytes()), "{}", input);
}
}
#[test]
fn it_should_mirror_valid_uris() {
let finder = URI::default();
for input in vec![
"http://localhost",
"http://foobar:@localhost",
"http://foobar:baz@localhost",
"http://foobar:@localhost:8080",
"http://localhost/lorem",
"http://foobar:@localhost:8080?a=b",
"http://foobar:@localhost:8080#",
"http://foobar:@localhost:8080?#",
"http://foobar:@localhost:8080?a=b#",
"http://foobar:@localhost:8080?a=b#c=d",
"http://:@localhost:/?#",
"http://127.0.0.0",
"http://127.0.0.10",
"http://127.0.0.100",
"http://127.0.0.200",
"http://127.0.0.250",
"http://192.0.2.235",
"http://[::]",
"http://[::1]",
"http://[2001:db8::1]",
"http://[2001:0db8::0001]",
"http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
"http://[::ffff:192.0.2.128]",
"http://[::ffff:c000:0280]",
"file:///etc/hosts",
"http://localhost/",
"mailto:fred@example.com",
"foo://info.example.com?fred",
"ftp://ftp.is.co.za/rfc/rfc1808.txt",
"http://www.ietf.org/rfc/rfc2396.txt",
"ldap://[2001:db8::7]/c=GB?objectClass?one",
"mailto:John.Doe@example.com",
"news:comp.infosystems.www.servers.unix",
"tel:+1-816-555-1212",
"telnet://192.0.2.16:80/",
"urn:oasis:names:specification:docbook:dtd:xml:4.1.2",
] {
for i in vec![
input.to_owned(),
format!(" {} ", input),
format!("<{}>", input),
format!("[{}]", input),
format!("<a href=\"{}\">link</a>", input),
format!("{{{}}}", input),
format!("\"{}\"", input),
format!("[link]({})", input),
format!("'{}'", input),
] {
assert_eq!(Some(input), finder.find(&i).map(|r| &i[r]), "{}", input);
}
}
}
#[test]
fn it_should_properly_behave_in_strict_mode() {
let mut finder = URI::default();
finder.strict = true;
for &input in &[
"http://localhost/)",
"http://localhost/'",
"http://foobar:@localhost:",
"http://foobar:@localhost:8080?",
"data:,Hello%2C%20World!",
] {
assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
}
}
#[test]
fn it_should_ignore_invalid_uris() {
let finder = URI::default();
for input in vec![
"ftp:///test",
"http:///test",
"https:///test",
] {
assert_eq!(None, finder.find(input), "{}", input);
}
}
#[test]
fn is_ipvfuture_should_identify_valid_ipvfutures() {
let finder = URI::default();
for input in vec![
"v1.test",
"vFF.a",
"v1.fe80",
"v1.a:b:c",
"vABCDEF.test",
"v1.unreserved-chars_here~",
"v1.sub!delims$and&more",
] {
assert_eq!(true, finder.is_ipvfuture(input.as_bytes()), "{}", input);
}
}
#[test]
fn is_ipvfuture_should_identify_invalid_ipvfutures() {
let finder = URI::default();
for input in vec![
"", "v", "v1", "v1.", "v.", "v.test", "1.test",
"vGG.test", ] {
assert_eq!(false, finder.is_ipvfuture(input.as_bytes()), "{}", input);
}
}
#[test]
fn it_should_mirror_valid_ipvfuture_uris() {
let finder = URI::default();
for input in vec![
"http://[v1.test]/path",
"http://[vFF.fe80]/",
"http://[v1.a:b:c]",
"http://[v1.test]:8080/path",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_data_uris() {
let finder = URI::default();
for input in vec![
"data:text/plain;base64,SGVsbG8=",
"data:image/png;base64,iVBORw0KGgo=",
"data:,Hello%2C%20World",
"data:text/html,%3Ch1%3EHello%3C%2Fh1%3E",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_mailto_uris() {
let finder = URI::default();
for input in vec![
"mailto:user@example.com",
"mailto:user@example.com?subject=Hello&body=World",
"mailto:user@example.com,other@example.com",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_tel_uris() {
let finder = URI::default();
for input in vec![
"tel:+1-816-555-1212",
"tel:+1-816-555-1212;ext=1234",
"tel:911",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_uris_with_percent_encoding() {
let finder = URI::default();
for input in vec![
"http://example.com/path%20with%20spaces",
"http://example.com/path?q=%E2%9C%93",
"http://user%40name:pass%23word@example.com",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_uris_with_fragments() {
let finder = URI::default();
for input in vec![
"http://example.com#section",
"http://example.com/page#",
"http://example.com?query#fragment",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_uris_with_empty_paths() {
let finder = URI::default();
for input in vec!["http://example.com?query", "http://example.com#fragment"] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_uris_with_multiple_slashes() {
let finder = URI::default();
for input in vec![
"http://example.com//path",
"http://example.com//path///to////resource",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_ipv4_mapped_ipv6_uris() {
let finder = URI::default();
for input in vec![
"http://[::ffff:192.168.1.1]",
"http://[::ffff:127.0.0.1]:8080/path",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_punycode_domains() {
let finder = URI::default();
for input in vec!["http://xn--n3h.com", "http://xn--nxasmq5b.com"] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_hostnames_with_underscores() {
let finder = URI::default();
for input in vec!["http://my_server.local", "http://_dmarc.example.com"] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_mirror_single_label_hostnames() {
let finder = URI::default();
for input in vec![
"http://localhost",
"http://intranet",
"http://localhost:8080",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn it_should_handle_empty_string() {
let finder = URI::default();
assert_eq!(None, finder.find(""));
}
#[test]
fn it_should_handle_string_without_uris() {
let finder = URI::default();
for input in vec![
"hello world",
"no urls here",
"just some text",
"12345", " ", "123:456", ":no-scheme", ] {
assert_eq!(None, finder.find(input), "{}", input);
}
}
#[test]
fn it_should_handle_very_long_uris() {
let finder = URI::default();
let long_path = format!("http://example.com/{}", "a".repeat(2000));
assert_eq!(
Some(long_path.as_str()),
finder.find(&long_path).map(|r| &long_path[r])
);
let long_query = format!("http://example.com?q={}", "b".repeat(2000));
assert_eq!(
Some(long_query.as_str()),
finder.find(&long_query).map(|r| &long_query[r])
);
let long_fragment = format!("http://example.com#{}", "c".repeat(2000));
assert_eq!(
Some(long_fragment.as_str()),
finder.find(&long_fragment).map(|r| &long_fragment[r])
);
}
#[test]
fn it_should_handle_malformed_percent_encoding() {
let finder = URI::default();
assert!(finder.find("http://example.com/%20").is_some());
assert!(finder.find("http://example.com/%2F").is_some());
let input = "http://example.com/path%GG";
let result = finder.find(input).map(|r| &input[r]);
assert_eq!(Some("http://example.com/path"), result);
let input2 = "http://example.com/path%ZZ";
let result2 = finder.find(input2).map(|r| &input2[r]);
assert_eq!(Some("http://example.com/path"), result2);
}
#[test]
fn it_should_handle_incomplete_percent_encoding() {
let finder = URI::default();
let input = "http://example.com/path%";
let result = finder.find(input).map(|r| &input[r]);
assert_eq!(Some("http://example.com/path"), result);
let input2 = "http://example.com/path%2";
let result2 = finder.find(input2).map(|r| &input2[r]);
assert_eq!(Some("http://example.com/path"), result2);
}
#[test]
fn it_should_handle_unicode_in_hostnames() {
let finder = URI::default();
for input in vec![
"http://xn--n3h.com", "http://xn--nxasmq5b.com", "http://xn--80ak6aa92e.com", ] {
assert!(finder.find(input).is_some(), "{}", input);
}
}
#[test]
fn it_should_handle_edge_case_ports() {
let finder = URI::default();
assert!(finder.find("http://example.com:").is_some());
assert!(finder.find("http://example.com:0").is_some());
assert!(finder.find("http://example.com:99999").is_some());
}
#[test]
fn it_should_handle_edge_case_userinfo() {
let finder = URI::default();
assert!(finder.find("http://@example.com").is_some());
assert!(finder.find("http://user@example.com").is_some());
assert!(finder.find("http://user:@example.com").is_some());
assert!(finder.find("http://:pass@example.com").is_some());
assert!(finder.find("http://user%40name@example.com").is_some());
}
#[test]
fn it_should_handle_consecutive_special_chars() {
let finder = URI::default();
assert!(finder.find("http://example.com?a=1&b=2&c=3").is_some());
let input = "http://example.com#frag#ment";
assert_eq!(
Some("http://example.com#frag"),
finder.find(input).map(|r| &input[r])
);
assert!(finder.find("http://example.com?query#fragment").is_some());
}
#[test]
fn it_should_handle_scheme_edge_cases() {
let finder = URI::default();
assert!(finder.find("h2c://example.com").is_some());
assert!(finder.find("coap+tcp://example.com").is_some());
assert!(finder.find("x.y.z://example.com").is_some());
assert!(finder.find("my-scheme://example.com").is_some());
assert!(finder.find("x://example.com").is_some());
}
#[test]
fn it_should_handle_ipv4_edge_cases() {
let finder = URI::default();
assert!(finder.find("http://0.0.0.0").is_some());
assert!(finder.find("http://255.255.255.255").is_some());
let input = "http://256.0.0.1";
assert!(finder.find(input).is_some());
assert!(finder.find("http://192.168.001.001").is_some());
}
#[test]
fn it_should_handle_multiple_uris_in_text() {
let finder = URI::default();
let text = "Visit http://first.com and https://second.com today";
let result = finder.find(text);
assert!(result.is_some());
assert_eq!("http://first.com", &text[result.unwrap()]);
}
#[test]
fn it_should_handle_uri_at_string_boundaries() {
let finder = URI::default();
let input1 = "http://example.com is a URL";
assert_eq!(
Some("http://example.com"),
finder.find(input1).map(|r| &input1[r])
);
let input2 = "Visit http://example.com";
assert_eq!(
Some("http://example.com"),
finder.find(input2).map(|r| &input2[r])
);
let input3 = "http://example.com";
assert_eq!(
Some("http://example.com"),
finder.find(input3).map(|r| &input3[r])
);
}
#[test]
fn it_should_filter_by_scheme() {
let mut finder = URI::default();
finder.add_scheme("https");
let input1 = "https://secure.com";
assert_eq!(Some(input1), finder.find(input1).map(|r| &input1[r]));
let input2 = "http://insecure.com";
assert_eq!(None, finder.find(input2));
let input3 = "http://first.com https://second.com";
assert_eq!(
Some("https://second.com"),
finder.find(input3).map(|r| &input3[r])
);
}
#[test]
fn it_should_handle_scheme_case_insensitivity() {
let mut finder = URI::default();
finder.add_scheme("HTTP");
let input = "http://example.com";
assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
let input = "HTTP://example.com";
assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
}
#[test]
fn uppercase_http_scheme_obeys_host_rules() {
let finder = URI::default();
assert_eq!(None, finder.find("HTTP:///missing-host"));
assert_eq!(None, finder.find("HTTPS:///missing-host"));
}
#[test]
fn add_scheme_deduplicates() {
let mut finder = URI::default();
finder.add_scheme("http");
finder.add_scheme("http");
finder.add_scheme("HTTP");
assert_eq!(finder.schemes.len(), 1);
}
#[test]
fn add_scheme_multiple_sorted() {
let mut finder = URI::default();
finder.add_scheme("https");
finder.add_scheme("ftp");
finder.add_scheme("http");
assert_eq!(finder.schemes, vec!["ftp", "http", "https"]);
}
#[test]
fn look_pchar_unreserved_chars() {
let finder = URI::default();
for input in [
"http://example.com/path-with-dashes_and_underscores.and.dots~tilde",
"http://example.com/ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"http://example.com/abcdefghijklmnopqrstuvwxyz",
"http://example.com/0123456789",
] {
assert_eq!(
Some(input),
finder.find(input).map(|r| &input[r]),
"{}",
input
);
}
}
#[test]
fn look_pchar_pct_encoded() {
let finder = URI::default();
let input = "http://example.com/%20%2F%3A";
assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
}
#[test]
fn look_pchar_sub_delims() {
let finder = URI::default();
let input = "http://example.com/path!$&(*+,;=";
assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
}
#[test]
fn rlook_scheme_very_long_prefix() {
let finder = URI::default();
let long_prefix = " ".repeat(1000);
let input = format!("{}http://example.com", long_prefix);
assert_eq!(
Some("http://example.com"),
finder.find(&input).map(|r| &input[r])
);
}
#[test]
fn rlook_scheme_just_within_cap() {
let finder = URI::default();
let scheme = "a".repeat(30);
let input = format!("{}://host", scheme);
assert_eq!(Some(input.as_str()), finder.find(&input).map(|r| &input[r]));
}
#[test]
fn registered_schemes_should_fit_the_scheme_lookback() {
for scheme in crate::iana::URI_SCHEMES.iter() {
assert!(scheme.len() <= MAX_SCHEME_LEN, "{scheme}");
}
}
#[test]
fn find_should_keep_long_registered_opaque_uris() {
let finder = URI::default();
let input = "open microsoft.windows.camera.multipicker:photo now";
assert_eq!(
Some("microsoft.windows.camera.multipicker:photo"),
finder.find(input).map(|r| &input[r])
);
}
#[test]
fn rlook_scheme_exceeds_cap() {
let finder = URI::default();
let scheme = "a".repeat(MAX_SCHEME_LEN + 8);
let input = format!("{}://host", scheme);
let result = finder.find(&input);
assert!(result.is_some());
}
#[test]
fn sub_delim_strict_vs_lax() {
let strict = {
let mut f = URI::default();
f.strict = true;
f
};
let lax = URI::default();
let with_apos = "http://example.com/it's";
assert_eq!(
Some("http://example.com/it's"),
strict.find(with_apos).map(|r| &with_apos[r])
);
assert_eq!(
Some("http://example.com/it"),
lax.find(with_apos).map(|r| &with_apos[r])
);
let with_paren = "http://example.com/path)";
assert_eq!(
Some("http://example.com/path)"),
strict.find(with_paren).map(|r| &with_paren[r])
);
assert_eq!(
Some("http://example.com/path"),
lax.find(with_paren).map(|r| &with_paren[r])
);
}
}