1use super::Finder;
21use std::ops::Range;
22
23const SUB_DELIMS_STRICT: [bool; 256] = {
24 let mut table = [false; 256];
25 table[b'!' as usize] = true;
26 table[b'$' as usize] = true;
27 table[b'&' as usize] = true;
28 table[b'\'' as usize] = true;
29 table[b'(' as usize] = true;
30 table[b')' as usize] = true;
31 table[b'*' as usize] = true;
32 table[b'+' as usize] = true;
33 table[b',' as usize] = true;
34 table[b';' as usize] = true;
35 table[b'=' as usize] = true;
36 table
37};
38
39const SUB_DELIMS_LAX: [bool; 256] = {
40 let mut table = [false; 256];
41 table[b'!' as usize] = true;
42 table[b'$' as usize] = true;
43 table[b'&' as usize] = true;
44 table[b'(' as usize] = true;
45 table[b'*' as usize] = true;
46 table[b'+' as usize] = true;
47 table[b',' as usize] = true;
48 table[b';' as usize] = true;
49 table[b'=' as usize] = true;
50 table
51};
52
53const PCHAR: u8 = 1 << 0;
58const QUERY: u8 = 1 << 1;
59const USERINFO: u8 = 1 << 2;
60const LABEL: u8 = 1 << 3;
62
63const fn body_classes(strict: bool) -> [u8; 256] {
64 let mut table = [0u8; 256];
65 let mut b = 0usize;
66 while b < 256 {
67 let c = b as u8;
68 let unreserved =
69 c.is_ascii_alphanumeric() || c == b'-' || c == b'.' || c == b'_' || c == b'~';
70 let sub_delim = if strict {
71 SUB_DELIMS_STRICT[b]
72 } else {
73 SUB_DELIMS_LAX[b]
74 };
75 let mut flags = 0u8;
76 if unreserved || sub_delim || c == b':' {
77 flags |= USERINFO;
78 }
79 if unreserved || sub_delim || c == b':' || c == b'@' || (!strict && c == b')') {
80 flags |= PCHAR;
81 }
82 if flags & PCHAR != 0 || c == b'/' || c == b'?' {
83 flags |= QUERY;
84 }
85 if c.is_ascii_alphanumeric() || c == b'_' || c == b'-' {
86 flags |= LABEL;
87 }
88 table[b] = flags;
89 b += 1;
90 }
91 table
92}
93
94static BODY_CLASSES_STRICT: [u8; 256] = body_classes(true);
95static BODY_CLASSES_LAX: [u8; 256] = body_classes(false);
96
97const MAX_SCHEME_LEN: usize = 64;
100
101#[derive(Default, Clone, Copy)]
102struct SchemeConfig(u8);
103
104impl SchemeConfig {
105 fn has(&self, flag: u8) -> bool {
106 (self.0 & flag) != 0
107 }
108}
109
110fn scheme_config(scheme: &str) -> SchemeConfig {
112 if ["ftp", "http", "https"]
113 .iter()
114 .any(|s| scheme.eq_ignore_ascii_case(s))
115 {
116 SchemeConfig(DISALLOW_EMPTY_HOST)
117 } else {
118 SchemeConfig::default()
119 }
120}
121
122fn can_end_registered_scheme(prev2: u8, prev1: u8) -> bool {
128 static TABLE: std::sync::OnceLock<Box<[u64; 1024]>> = std::sync::OnceLock::new();
129 let table = TABLE.get_or_init(|| {
130 let mut table = Box::new([0u64; 1024]);
131 let mut set = |a: u8, b: u8| {
132 let index = usize::from(a) << 8 | usize::from(b);
133 table[index >> 6] |= 1 << (index & 63);
134 };
135 for scheme in crate::iana::URI_SCHEMES.iter() {
136 let bytes = scheme.as_bytes();
137 match bytes {
138 [] => {}
139 [only] => {
140 for a in 0..=255u8 {
141 set(a, *only);
142 }
143 }
144 [.., a, b] => set(*a, *b),
145 }
146 }
147 table
148 });
149 let index =
150 usize::from(prev2.to_ascii_lowercase()) << 8 | usize::from(prev1.to_ascii_lowercase());
151 table[index >> 6] & (1 << (index & 63)) != 0
152}
153
154fn is_registered_scheme(scheme: &str) -> bool {
155 crate::registry::uri_schemes().contains(scheme.as_bytes())
156}
157
158const DISALLOW_EMPTY_HOST: u8 = 1 << 0;
159
160#[derive(Default)]
176pub struct URI {
177 schemes: Vec<String>,
178 scheme_ends: [u64; 4],
180 pub strict: bool,
184}
185
186impl Finder for URI {
187 fn id(&self) -> &'static str {
188 "uri"
189 }
190
191 fn triggerable(&self) -> bool {
192 true
193 }
194
195 fn line_agnostic(&self) -> bool {
196 true
198 }
199
200 fn could_trigger_at(&self, byte: u8) -> bool {
201 byte == b':'
202 }
203
204 fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
205 Self::is_alpha(prev) || Self::is_digit(prev) || matches!(prev, b'+' | b'-' | b'.')
207 }
208
209 fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
210 if self.strict {
211 return true;
212 }
213 next.is_ascii_alphanumeric()
216 || matches!(
217 next,
218 b'-' | b'.'
219 | b'_'
220 | b'~'
221 | b'%'
222 | b'!'
223 | b'$'
224 | b'&'
225 | b'\''
226 | b'('
227 | b')'
228 | b'*'
229 | b'+'
230 | b','
231 | b';'
232 | b'='
233 | b'@'
234 | b'/'
235 | b'?'
236 | b'#'
237 )
238 }
239
240 fn has_trigger_context(&self) -> bool {
241 !self.strict || !self.schemes.is_empty()
245 }
246
247 fn trigger_context(&self, prev2: u8, prev1: u8) -> bool {
249 if !self.schemes.is_empty() {
250 let (prev2, prev1) = (prev2.to_ascii_lowercase(), prev1.to_ascii_lowercase());
251 if self.scheme_ends[usize::from(prev1 >> 6)] & (1 << (prev1 & 63)) == 0 {
252 return false;
253 }
254 return self.schemes.iter().any(|s| match s.as_bytes() {
255 [] => false,
256 [only] => *only == prev1,
257 [.., a, b] => *a == prev2 && *b == prev1,
258 });
259 }
260 self.strict || can_end_registered_scheme(prev2, prev1)
261 }
262
263 fn trigger_context_exempt(&self, next: Option<u8>) -> bool {
264 self.schemes.is_empty() && next == Some(b'/')
265 }
266
267 fn try_trigger_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
268 self.try_at_colon(input, pos)
269 }
270
271 fn find(&self, s: &str) -> Option<Range<usize>> {
273 let input = s.as_bytes();
274 let mut idx = 0;
275
276 while idx < input.len() {
277 let colon_idx = idx + input[idx..].iter().position(|&b| b == b':')?;
278 idx = colon_idx + 1;
279 if let Some(range) = self.try_at_colon(input, colon_idx) {
280 return Some(range);
281 }
282 }
283
284 None
285 }
286}
287
288impl URI {
289 pub fn add_scheme(&mut self, s: &str) {
306 let lower = s.to_lowercase();
307 if let Some(&last) = lower.as_bytes().last() {
308 self.scheme_ends[usize::from(last >> 6)] |= 1 << (last & 63);
309 }
310 if let Err(pos) = self.schemes.binary_search(&lower) {
311 self.schemes.insert(pos, lower);
312 }
313 }
314
315 fn try_at_colon(&self, input: &[u8], colon_idx: usize) -> Option<Range<usize>> {
316 if input[colon_idx] != b':' {
317 return None;
318 }
319
320 if !self.strict
324 && self.schemes.is_empty()
325 && input.get(colon_idx + 1) != Some(&b'/')
326 && colon_idx >= 1
327 {
328 let prev1 = input[colon_idx - 1];
329 let prev2 = if colon_idx >= 2 {
330 input[colon_idx - 2]
331 } else {
332 b' '
333 };
334 if !can_end_registered_scheme(prev2, prev1) {
335 return None;
336 }
337 }
338
339 let scheme_idx = self.rlook_scheme(&input[..colon_idx])?;
340 let scheme = std::str::from_utf8(&input[scheme_idx..colon_idx]).ok()?;
341 if !self.schemes.is_empty() && !self.schemes.iter().any(|s| s.eq_ignore_ascii_case(scheme))
343 {
344 return None;
345 }
346 if !self.strict
350 && self.schemes.is_empty()
351 && !input[colon_idx + 1..].starts_with(b"//")
352 && !is_registered_scheme(scheme)
353 {
354 return None;
355 }
356 let scheme_config = scheme_config(scheme);
357
358 if !self.strict && input.get(colon_idx + 1) == Some(&b':') {
361 return None;
362 }
363
364 let mut idx = colon_idx + 1;
365 let hier_len = self.look_hier_part(&input[idx..], scheme_config)?;
366 idx += hier_len;
367 idx += self.look_question_mark_query(&input[idx..]).unwrap_or(0);
368 idx += self.look_sharp_fragment(&input[idx..]).unwrap_or(0);
369
370 if !self.strict {
371 idx = Self::trim_lax(input, colon_idx, idx);
372 let tail = &input[colon_idx + 1..idx];
373 if tail
376 .iter()
377 .all(|&b| matches!(b, b'.' | b',' | b';' | b':' | b'!' | b'?' | b'#'))
378 {
379 return None;
380 }
381 if hier_len == 0 && scheme_idx > 0 && !Self::is_prose_delimiter(input[scheme_idx - 1]) {
387 return None;
388 }
389 if self.schemes.is_empty() && !tail.starts_with(b"//") && !is_registered_scheme(scheme)
393 {
394 return None;
395 }
396 }
397
398 Some(scheme_idx..idx)
399 }
400
401 fn trim_lax(input: &[u8], colon_idx: usize, mut end: usize) -> usize {
405 let body_start = colon_idx + 1;
406
407 if memchr::memchr(b')', &input[body_start..end]).is_some() {
409 let mut depth = 0usize;
410 for (i, &b) in input[body_start..end].iter().enumerate() {
411 match b {
412 b'(' => depth += 1,
413 b')' => {
414 if depth == 0 {
415 end = body_start + i;
416 break;
417 }
418 depth -= 1;
419 }
420 _ => {}
421 }
422 }
423 }
424
425 while end > body_start && matches!(input[end - 1], b'.' | b',' | b';' | b':' | b'!' | b'?')
426 {
427 end -= 1;
428 }
429
430 end
431 }
432
433 #[inline]
436 fn is_prose_delimiter(b: u8) -> bool {
437 b <= b' '
438 || b >= 0x80
439 || matches!(
440 b,
441 b'"' | b'\'' | b'<' | b'>' | b'\\' | b'^' | b'`' | b'{' | b'|' | b'}' | b'(' | b'['
442 )
443 }
444
445 fn rlook_scheme(&self, input: &[u8]) -> Option<usize> {
447 let start = input.len().saturating_sub(MAX_SCHEME_LEN);
448 let mut idx = None;
449 let mut run_start = input.len();
450 for (i, &c) in input[start..].iter().enumerate().rev() {
451 if Self::is_alpha(c) {
452 idx = Some(start + i);
453 run_start = start + i;
454 } else if Self::is_digit(c) || c == b'+' || c == b'-' || c == b'.' {
455 run_start = start + i;
456 } else {
457 break;
458 }
459 }
460 if run_start < input.len() && Self::is_digit(input[run_start]) {
464 return None;
465 }
466 idx
467 }
468
469 fn look_hier_part(&self, input: &[u8], sc: SchemeConfig) -> Option<usize> {
474 if let Some(idx) = self.look_slash_slash(input) {
480 let idx = idx + self.look_authority(&input[idx..], sc)?;
481 return Some(idx + self.look_path_abempty(&input[idx..]));
482 }
483
484 if sc.has(DISALLOW_EMPTY_HOST) {
486 return None;
487 }
488
489 if let Some(idx) = self.look_slash(input).map(|idx| {
491 idx + self
492 .look_segment_nz(&input[idx..])
493 .map(|i| i + self.look_path_abempty(&input[idx + i..]))
494 .unwrap_or(0)
495 }) {
496 return Some(idx);
497 }
498
499 if let Some(idx) = self
501 .look_segment_nz(input)
502 .map(|idx| idx + self.look_path_abempty(&input[idx..]))
503 {
504 return Some(idx);
505 }
506
507 Some(0)
509 }
510
511 fn look_authority(&self, input: &[u8], sc: SchemeConfig) -> Option<usize> {
513 let mut idx = 0;
514 idx += self.look_userinfo_at(&input[idx..]).unwrap_or(0);
515 idx += self
516 .look_host(&input[idx..])
517 .filter(|&i| !(i == 0 && sc.has(DISALLOW_EMPTY_HOST)))?;
518 if let Some(i) = self.look_colon(&input[idx..]) {
519 idx += i;
520 let port_len = self.look_port(&input[idx..]);
521 idx += port_len;
522 let glued = input.get(idx).is_some_and(|&b| {
526 b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || (b == b'.' && port_len > 0)
527 });
528 if glued {
529 return None;
530 }
531 }
532 Some(idx)
533 }
534
535 fn look_path_abempty(&self, input: &[u8]) -> usize {
537 let mut idx = 0;
538 while idx < input.len() {
539 idx += match self
540 .look_slash(&input[idx..])
541 .map(|i| i + self.look_segment(&input[idx + i..]))
542 {
543 Some(n) => n,
544 None => break,
545 };
546 }
547 idx
548 }
549
550 fn look_segment(&self, input: &[u8]) -> usize {
552 self.look_run(input, PCHAR)
553 }
554
555 #[inline]
558 fn look_run(&self, input: &[u8], class: u8) -> usize {
559 let table = self.body_classes();
560 let mut idx = 0;
561 while idx < input.len() {
562 let b = input[idx];
563 if table[b as usize] & class != 0 {
564 idx += 1;
565 } else if b == b'%' {
566 match self.look_pct_encoded(&input[idx..]) {
567 Some(n) => idx += n,
568 None => break,
569 }
570 } else {
571 break;
572 }
573 }
574 idx
575 }
576
577 #[inline]
578 fn body_classes(&self) -> &'static [u8; 256] {
579 if self.strict {
580 &BODY_CLASSES_STRICT
581 } else {
582 &BODY_CLASSES_LAX
583 }
584 }
585
586 fn look_segment_nz(&self, input: &[u8]) -> Option<usize> {
588 match self.look_segment(input) {
589 0 => None,
590 n => Some(n),
591 }
592 }
593
594 fn look_userinfo_at(&self, input: &[u8]) -> Option<usize> {
599 let table = self.body_classes();
600 let mut idx = 0;
601 while idx < input.len() && idx < 256 {
602 let b = input[idx];
603 if b == b'@' {
604 return Some(idx + 1);
605 }
606 if table[b as usize] & USERINFO != 0 {
607 idx += 1;
608 } else if b == b'%' {
609 idx += self.look_pct_encoded(&input[idx..])?;
610 } else {
611 return None;
612 }
613 }
614 None
615 }
616
617 fn look_host(&self, input: &[u8]) -> Option<usize> {
619 if let Some(idx) = self.look_ip_literal(input) {
620 if input
624 .get(idx)
625 .is_some_and(|&b| b.is_ascii_alphanumeric() || b == b'.' || b == b'+' || b == b'-')
626 {
627 return None;
628 }
629 return Some(idx);
630 }
631 if let Some(idx) = self.look_ipv4_address(input) {
632 match input.get(idx) {
633 Some(&b) if b == b'.' || b == b'-' || b == b'_' || b.is_ascii_alphanumeric() => {}
636 Some(&b) if b >= 0x80 => return None,
638 _ => return Some(idx),
639 }
640 }
641 self.look_hostname(input)
642 }
643
644 fn look_ip_literal(&self, input: &[u8]) -> Option<usize> {
646 let mut idx = 0;
647 idx += self.look_left_bracket(&input[idx..])?;
648 let right_bracket_index = input[idx..].iter().take(64).position(|&b| b == b']')?;
649 if right_bracket_index > 0 {
650 let end = idx + right_bracket_index;
651 let slice = &input[idx..end];
652 if self.is_ipv6address(slice) || self.is_ipv6addrz(slice) || self.is_ipvfuture(slice) {
653 return Some(end + 1);
654 }
655 }
656 None
657 }
658
659 fn is_ipv6addrz(&self, input: &[u8]) -> bool {
662 let Some(pct) = input.iter().position(|&b| b == b'%') else {
663 return false;
664 };
665 if !self.is_ipv6address(&input[..pct]) {
666 return false;
667 }
668 let mut zone = &input[pct + 1..];
669 if zone.starts_with(b"25") && zone.len() > 2 {
670 zone = &zone[2..]; }
672 !zone.is_empty() && zone.iter().all(|&b| Self::is_unreserved(b))
673 }
674
675 fn is_ipv6address(&self, input: &[u8]) -> bool {
678 crate::ipv6::is_valid_ipv6(input)
679 }
680
681 fn is_ipvfuture(&self, input: &[u8]) -> bool {
683 if input.is_empty() || input[0] != b'v' {
684 return false;
685 }
686
687 let mut idx = 1;
688
689 let hexdig_start = idx;
691 while idx < input.len() && Self::is_hexdig(input[idx]) {
692 idx += 1;
693 }
694 if idx == hexdig_start {
695 return false; }
697
698 if idx >= input.len() || input[idx] != b'.' {
700 return false;
701 }
702 idx += 1;
703
704 let char_start = idx;
706 while idx < input.len() {
707 let c = input[idx];
708 if Self::is_unreserved(c) || Self::is_sub_delim_strict(c) || c == b':' {
709 idx += 1;
710 } else {
711 break;
712 }
713 }
714 if idx == char_start {
715 return false; }
717
718 idx == input.len()
719 }
720
721 #[inline]
722 fn is_sub_delim_strict(c: u8) -> bool {
723 SUB_DELIMS_STRICT[c as usize]
724 }
725
726 fn look_ipv4_address(&self, input: &[u8]) -> Option<usize> {
728 let mut idx = 0;
729 idx += self.look_dec_octet(&input[idx..])?;
730 idx += self.look_period(&input[idx..])?;
731 idx += self.look_dec_octet(&input[idx..])?;
732 idx += self.look_period(&input[idx..])?;
733 idx += self.look_dec_octet(&input[idx..])?;
734 idx += self.look_period(&input[idx..])?;
735 idx += self.look_dec_octet(&input[idx..])?;
736 Some(idx)
737 }
738
739 fn look_dec_octet(&self, input: &[u8]) -> Option<usize> {
745 if input.len() >= 3
746 && input[0] == b'2'
747 && input[1] == b'5'
748 && Self::is_digit_0_to_5(input[2])
749 {
750 return Some(3);
751 }
752
753 if input.len() >= 3
754 && input[0] == b'2'
755 && Self::is_digit_0_to_4(input[1])
756 && Self::is_digit(input[2])
757 {
758 return Some(3);
759 }
760
761 if input.len() >= 3
762 && input[0] == b'1'
763 && Self::is_digit(input[1])
764 && Self::is_digit(input[2])
765 {
766 return Some(3);
767 }
768
769 if input.len() >= 2 && Self::is_digit_1_to_9(input[0]) && Self::is_digit(input[1]) {
770 return Some(2);
771 }
772
773 if !input.is_empty() && Self::is_digit(input[0]) {
774 return Some(1);
775 }
776
777 None
778 }
779
780 fn look_hostname(&self, input: &[u8]) -> Option<usize> {
786 const MAX_LABEL: usize = 63; const MAX_HOSTNAME: usize = 253;
788
789 let mut idx = 0;
790 loop {
791 let run = Self::label_run_len(&input[idx..]);
792 if run == 0 {
793 break; }
795 if run > MAX_LABEL {
796 return None;
797 }
798 idx += run;
799 if input.get(idx) == Some(&b'.') && Self::label_run_len(&input[idx + 1..]) > 0 {
802 idx += 1;
803 continue;
804 }
805 break;
806 }
807 if idx > MAX_HOSTNAME {
808 return None;
809 }
810 match input.get(idx) {
813 Some(&b) if b >= 0x80 => return None,
814 Some(&b'.') if input.get(idx + 1).is_some_and(|&b| b >= 0x80) => return None,
815 _ => {}
816 }
817 Some(idx)
818 }
819
820 fn label_run_len(input: &[u8]) -> usize {
823 let starts = input
824 .first()
825 .is_some_and(|&b| b.is_ascii_alphanumeric() || b == b'_');
826 if !starts {
827 return 0;
828 }
829 let table = &BODY_CLASSES_LAX;
831 let mut len = 1;
832 while len < input.len() && table[input[len] as usize] & LABEL != 0 {
833 len += 1;
834 }
835 len
836 }
837
838 fn look_port(&self, input: &[u8]) -> usize {
840 input.iter().take_while(|&&c| Self::is_digit(c)).count()
841 }
842
843 fn look_question_mark_query(&self, input: &[u8]) -> Option<usize> {
844 let mut idx = 0;
845 idx += self.look_question_mark(&input[idx..])?;
846 idx += self.look_query(&input[idx..]);
847 Some(idx)
848 }
849
850 fn look_query(&self, input: &[u8]) -> usize {
852 self.look_run(input, QUERY)
853 }
854
855 fn look_sharp_fragment(&self, input: &[u8]) -> Option<usize> {
856 let mut idx = 0;
857 idx += self.look_sharp(&input[idx..])?;
858 idx += self.look_fragment(&input[idx..]);
859 Some(idx)
860 }
861
862 fn look_fragment(&self, input: &[u8]) -> usize {
864 self.look_run(input, QUERY)
865 }
866
867 #[inline]
869 fn look_pct_encoded(&self, input: &[u8]) -> Option<usize> {
870 if input.len() >= 3
871 && input[0] == b'%'
872 && Self::is_hexdig(input[1])
873 && Self::is_hexdig(input[2])
874 {
875 Some(3)
876 } else {
877 None
878 }
879 }
880
881 #[inline]
882 fn look_period(&self, input: &[u8]) -> Option<usize> {
883 if !input.is_empty() && input[0] == b'.' {
884 Some(1)
885 } else {
886 None
887 }
888 }
889
890 #[inline]
891 fn look_left_bracket(&self, input: &[u8]) -> Option<usize> {
892 if !input.is_empty() && input[0] == b'[' {
893 Some(1)
894 } else {
895 None
896 }
897 }
898
899 #[inline]
900 fn look_colon(&self, input: &[u8]) -> Option<usize> {
901 if !input.is_empty() && input[0] == b':' {
902 Some(1)
903 } else {
904 None
905 }
906 }
907
908 #[inline]
909 fn look_question_mark(&self, input: &[u8]) -> Option<usize> {
910 if !input.is_empty() && input[0] == b'?' {
911 Some(1)
912 } else {
913 None
914 }
915 }
916
917 #[inline]
918 fn look_sharp(&self, input: &[u8]) -> Option<usize> {
919 if !input.is_empty() && input[0] == b'#' {
920 Some(1)
921 } else {
922 None
923 }
924 }
925
926 #[inline]
927 fn look_slash(&self, input: &[u8]) -> Option<usize> {
928 if !input.is_empty() && input[0] == b'/' {
929 Some(1)
930 } else {
931 None
932 }
933 }
934
935 #[inline]
936 fn look_slash_slash(&self, input: &[u8]) -> Option<usize> {
937 if input.len() >= 2 && input[0] == b'/' && input[1] == b'/' {
938 Some(2)
939 } else {
940 None
941 }
942 }
943
944 #[inline]
946 fn is_unreserved(c: u8) -> bool {
947 c.is_ascii_alphanumeric() || c == b'-' || c == b'.' || c == b'_' || c == b'~'
948 }
949
950 #[inline]
951 fn is_alpha(c: u8) -> bool {
952 c.is_ascii_alphabetic()
953 }
954
955 #[inline]
956 fn is_digit(c: u8) -> bool {
957 c.is_ascii_digit()
958 }
959
960 #[inline]
961 fn is_digit_1_to_9(c: u8) -> bool {
962 (b'1'..=b'9').contains(&c)
963 }
964
965 #[inline]
966 fn is_digit_0_to_4(c: u8) -> bool {
967 (b'0'..=b'4').contains(&c)
968 }
969
970 #[inline]
971 fn is_digit_0_to_5(c: u8) -> bool {
972 (b'0'..=b'5').contains(&c)
973 }
974
975 #[inline]
976 fn is_hexdig(c: u8) -> bool {
977 c.is_ascii_hexdigit()
978 }
979}
980
981#[cfg(test)]
982#[allow(clippy::bool_assert_comparison)]
983#[allow(clippy::useless_vec)]
984#[allow(clippy::field_reassign_with_default)]
985mod tests {
986 use super::*;
987
988 #[test]
989 fn is_ipv6address_should_identify_valid_ipv6s() {
990 let finder = URI::default();
991 for input in [
992 "::",
993 "::1",
994 "1::",
995 "1:2:3:4:5:6:7:8",
996 "1:2:3:4:5:6::7",
997 "1:2:3:4:5:6:127.0.0.1",
998 "1::127.0.0.1",
999 ] {
1000 assert_eq!(true, finder.is_ipv6address(input.as_bytes()), "{}", input);
1001 }
1002 }
1003
1004 #[test]
1005 fn is_ipv6address_should_identify_invalid_ipv6s() {
1006 let finder = URI::default();
1007 for input in vec![
1008 " ",
1009 " ::",
1010 ":: ",
1011 " :: ",
1012 ":::",
1013 "::1::",
1014 ":1:",
1015 "1:2:3:4:5:6:7:8:9",
1016 "1:2:3:4:5:6:7:127.0.0.1",
1017 "1:2:3:4:5:6::7:8",
1018 "1:2:3:4:5:6::127.0.0.1",
1019 "1:127.0.0.1::",
1020 ] {
1021 assert_eq!(false, finder.is_ipv6address(input.as_bytes()), "{}", input);
1022 }
1023 }
1024
1025 #[test]
1026 fn look_path_abempty_should_mirror_the_len_of_valid_inputs() {
1027 let finder = URI::default();
1028 for input in vec![
1029 "",
1030 "/",
1031 "//",
1032 "///",
1033 "/foo/bar",
1034 "/rfc/rfc1808.txt",
1035 "/with/trailing/",
1036 ] {
1037 assert_eq!(
1038 input.len(),
1039 finder.look_path_abempty(input.as_bytes()),
1040 "{}",
1041 input
1042 );
1043 }
1044 }
1045
1046 #[test]
1047 fn look_path_abempty_should_skip_invalid_inputs() {
1048 let finder = URI::default();
1049 for input in vec!["foobar"] {
1050 assert_eq!(0, finder.look_path_abempty(input.as_bytes()), "{}", input);
1051 }
1052 }
1053
1054 #[test]
1055 fn it_should_mirror_valid_uris() {
1056 let finder = URI::default();
1057 for input in vec![
1058 "http://localhost",
1060 "http://foobar:@localhost",
1062 "http://foobar:baz@localhost",
1063 "http://foobar:@localhost:8080",
1065 "http://localhost/lorem",
1067 "http://foobar:@localhost:8080?a=b",
1069 "http://foobar:@localhost:8080#",
1071 "http://foobar:@localhost:8080?#",
1072 "http://foobar:@localhost:8080?a=b#",
1073 "http://foobar:@localhost:8080?a=b#c=d",
1074 "http://:@localhost:/?#",
1076 "http://127.0.0.0",
1078 "http://127.0.0.10",
1079 "http://127.0.0.100",
1080 "http://127.0.0.200",
1081 "http://127.0.0.250",
1082 "http://192.0.2.235",
1083 "http://[::]",
1085 "http://[::1]",
1086 "http://[2001:db8::1]",
1087 "http://[2001:0db8::0001]",
1088 "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
1089 "http://[::ffff:192.0.2.128]",
1090 "http://[::ffff:c000:0280]",
1091 "file:///etc/hosts",
1093 "http://localhost/",
1094 "mailto:fred@example.com",
1095 "foo://info.example.com?fred",
1096 "ftp://ftp.is.co.za/rfc/rfc1808.txt",
1097 "http://www.ietf.org/rfc/rfc2396.txt",
1098 "ldap://[2001:db8::7]/c=GB?objectClass?one",
1099 "mailto:John.Doe@example.com",
1100 "news:comp.infosystems.www.servers.unix",
1101 "tel:+1-816-555-1212",
1102 "telnet://192.0.2.16:80/",
1103 "urn:oasis:names:specification:docbook:dtd:xml:4.1.2",
1104 ] {
1105 for i in vec![
1106 input.to_owned(),
1107 format!(" {} ", input),
1108 format!("<{}>", input),
1109 format!("[{}]", input),
1110 format!("<a href=\"{}\">link</a>", input),
1111 format!("{{{}}}", input),
1112 format!("\"{}\"", input),
1113 format!("[link]({})", input),
1114 format!("'{}'", input),
1115 ] {
1116 assert_eq!(Some(input), finder.find(&i).map(|r| &i[r]), "{}", input);
1117 }
1118 }
1119 }
1120
1121 #[test]
1122 fn it_should_properly_behave_in_strict_mode() {
1123 let mut finder = URI::default();
1124 finder.strict = true;
1125 for &input in &[
1126 "http://localhost/)",
1127 "http://localhost/'",
1128 "http://foobar:@localhost:",
1131 "http://foobar:@localhost:8080?",
1132 "data:,Hello%2C%20World!",
1133 ] {
1134 assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
1135 }
1136 }
1137
1138 #[test]
1139 fn it_should_ignore_invalid_uris() {
1140 let finder = URI::default();
1141 for input in vec![
1142 "ftp:///test",
1144 "http:///test",
1145 "https:///test",
1146 ] {
1147 assert_eq!(None, finder.find(input), "{}", input);
1148 }
1149 }
1150
1151 #[test]
1152 fn is_ipvfuture_should_identify_valid_ipvfutures() {
1153 let finder = URI::default();
1154 for input in vec![
1155 "v1.test",
1156 "vFF.a",
1157 "v1.fe80",
1158 "v1.a:b:c",
1159 "vABCDEF.test",
1160 "v1.unreserved-chars_here~",
1161 "v1.sub!delims$and&more",
1162 ] {
1163 assert_eq!(true, finder.is_ipvfuture(input.as_bytes()), "{}", input);
1164 }
1165 }
1166
1167 #[test]
1168 fn is_ipvfuture_should_identify_invalid_ipvfutures() {
1169 let finder = URI::default();
1170 for input in vec![
1171 "", "v", "v1", "v1.", "v.", "v.test", "1.test",
1172 "vGG.test", ] {
1174 assert_eq!(false, finder.is_ipvfuture(input.as_bytes()), "{}", input);
1175 }
1176 }
1177
1178 #[test]
1179 fn it_should_mirror_valid_ipvfuture_uris() {
1180 let finder = URI::default();
1181 for input in vec![
1182 "http://[v1.test]/path",
1183 "http://[vFF.fe80]/",
1184 "http://[v1.a:b:c]",
1185 "http://[v1.test]:8080/path",
1186 ] {
1187 assert_eq!(
1188 Some(input),
1189 finder.find(input).map(|r| &input[r]),
1190 "{}",
1191 input
1192 );
1193 }
1194 }
1195
1196 #[test]
1197 fn it_should_mirror_data_uris() {
1198 let finder = URI::default();
1199 for input in vec![
1200 "data:text/plain;base64,SGVsbG8=",
1201 "data:image/png;base64,iVBORw0KGgo=",
1202 "data:,Hello%2C%20World",
1203 "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E",
1204 ] {
1205 assert_eq!(
1206 Some(input),
1207 finder.find(input).map(|r| &input[r]),
1208 "{}",
1209 input
1210 );
1211 }
1212 }
1213
1214 #[test]
1215 fn it_should_mirror_mailto_uris() {
1216 let finder = URI::default();
1217 for input in vec![
1218 "mailto:user@example.com",
1219 "mailto:user@example.com?subject=Hello&body=World",
1220 "mailto:user@example.com,other@example.com",
1221 ] {
1222 assert_eq!(
1223 Some(input),
1224 finder.find(input).map(|r| &input[r]),
1225 "{}",
1226 input
1227 );
1228 }
1229 }
1230
1231 #[test]
1232 fn it_should_mirror_tel_uris() {
1233 let finder = URI::default();
1234 for input in vec![
1235 "tel:+1-816-555-1212",
1236 "tel:+1-816-555-1212;ext=1234",
1237 "tel:911",
1238 ] {
1239 assert_eq!(
1240 Some(input),
1241 finder.find(input).map(|r| &input[r]),
1242 "{}",
1243 input
1244 );
1245 }
1246 }
1247
1248 #[test]
1249 fn it_should_mirror_uris_with_percent_encoding() {
1250 let finder = URI::default();
1251 for input in vec![
1252 "http://example.com/path%20with%20spaces",
1253 "http://example.com/path?q=%E2%9C%93",
1254 "http://user%40name:pass%23word@example.com",
1255 ] {
1256 assert_eq!(
1257 Some(input),
1258 finder.find(input).map(|r| &input[r]),
1259 "{}",
1260 input
1261 );
1262 }
1263 }
1264
1265 #[test]
1266 fn it_should_mirror_uris_with_fragments() {
1267 let finder = URI::default();
1268 for input in vec![
1269 "http://example.com#section",
1270 "http://example.com/page#",
1271 "http://example.com?query#fragment",
1272 ] {
1273 assert_eq!(
1274 Some(input),
1275 finder.find(input).map(|r| &input[r]),
1276 "{}",
1277 input
1278 );
1279 }
1280 }
1281
1282 #[test]
1283 fn it_should_mirror_uris_with_empty_paths() {
1284 let finder = URI::default();
1285 for input in vec!["http://example.com?query", "http://example.com#fragment"] {
1286 assert_eq!(
1287 Some(input),
1288 finder.find(input).map(|r| &input[r]),
1289 "{}",
1290 input
1291 );
1292 }
1293 }
1294
1295 #[test]
1296 fn it_should_mirror_uris_with_multiple_slashes() {
1297 let finder = URI::default();
1298 for input in vec![
1299 "http://example.com//path",
1300 "http://example.com//path///to////resource",
1301 ] {
1302 assert_eq!(
1303 Some(input),
1304 finder.find(input).map(|r| &input[r]),
1305 "{}",
1306 input
1307 );
1308 }
1309 }
1310
1311 #[test]
1312 fn it_should_mirror_ipv4_mapped_ipv6_uris() {
1313 let finder = URI::default();
1314 for input in vec![
1315 "http://[::ffff:192.168.1.1]",
1316 "http://[::ffff:127.0.0.1]:8080/path",
1317 ] {
1318 assert_eq!(
1319 Some(input),
1320 finder.find(input).map(|r| &input[r]),
1321 "{}",
1322 input
1323 );
1324 }
1325 }
1326
1327 #[test]
1328 fn it_should_mirror_punycode_domains() {
1329 let finder = URI::default();
1330 for input in vec!["http://xn--n3h.com", "http://xn--nxasmq5b.com"] {
1331 assert_eq!(
1332 Some(input),
1333 finder.find(input).map(|r| &input[r]),
1334 "{}",
1335 input
1336 );
1337 }
1338 }
1339
1340 #[test]
1341 fn it_should_mirror_hostnames_with_underscores() {
1342 let finder = URI::default();
1343 for input in vec!["http://my_server.local", "http://_dmarc.example.com"] {
1344 assert_eq!(
1345 Some(input),
1346 finder.find(input).map(|r| &input[r]),
1347 "{}",
1348 input
1349 );
1350 }
1351 }
1352
1353 #[test]
1354 fn it_should_mirror_single_label_hostnames() {
1355 let finder = URI::default();
1356 for input in vec![
1357 "http://localhost",
1358 "http://intranet",
1359 "http://localhost:8080",
1360 ] {
1361 assert_eq!(
1362 Some(input),
1363 finder.find(input).map(|r| &input[r]),
1364 "{}",
1365 input
1366 );
1367 }
1368 }
1369
1370 #[test]
1375 fn it_should_handle_empty_string() {
1376 let finder = URI::default();
1377 assert_eq!(None, finder.find(""));
1378 }
1379
1380 #[test]
1381 fn it_should_handle_string_without_uris() {
1382 let finder = URI::default();
1383 for input in vec![
1386 "hello world",
1387 "no urls here",
1388 "just some text",
1389 "12345", " ", "123:456", ":no-scheme", ] {
1394 assert_eq!(None, finder.find(input), "{}", input);
1395 }
1396 }
1397
1398 #[test]
1399 fn it_should_handle_very_long_uris() {
1400 let finder = URI::default();
1401
1402 let long_path = format!("http://example.com/{}", "a".repeat(2000));
1404 assert_eq!(
1405 Some(long_path.as_str()),
1406 finder.find(&long_path).map(|r| &long_path[r])
1407 );
1408
1409 let long_query = format!("http://example.com?q={}", "b".repeat(2000));
1411 assert_eq!(
1412 Some(long_query.as_str()),
1413 finder.find(&long_query).map(|r| &long_query[r])
1414 );
1415
1416 let long_fragment = format!("http://example.com#{}", "c".repeat(2000));
1418 assert_eq!(
1419 Some(long_fragment.as_str()),
1420 finder.find(&long_fragment).map(|r| &long_fragment[r])
1421 );
1422 }
1423
1424 #[test]
1425 fn it_should_handle_malformed_percent_encoding() {
1426 let finder = URI::default();
1427
1428 assert!(finder.find("http://example.com/%20").is_some());
1430 assert!(finder.find("http://example.com/%2F").is_some());
1431
1432 let input = "http://example.com/path%GG";
1434 let result = finder.find(input).map(|r| &input[r]);
1435 assert_eq!(Some("http://example.com/path"), result);
1436
1437 let input2 = "http://example.com/path%ZZ";
1438 let result2 = finder.find(input2).map(|r| &input2[r]);
1439 assert_eq!(Some("http://example.com/path"), result2);
1440 }
1441
1442 #[test]
1443 fn it_should_handle_incomplete_percent_encoding() {
1444 let finder = URI::default();
1445
1446 let input = "http://example.com/path%";
1448 let result = finder.find(input).map(|r| &input[r]);
1449 assert_eq!(Some("http://example.com/path"), result);
1450
1451 let input2 = "http://example.com/path%2";
1453 let result2 = finder.find(input2).map(|r| &input2[r]);
1454 assert_eq!(Some("http://example.com/path"), result2);
1455 }
1456
1457 #[test]
1458 fn it_should_handle_unicode_in_hostnames() {
1459 let finder = URI::default();
1460
1461 for input in vec![
1463 "http://xn--n3h.com", "http://xn--nxasmq5b.com", "http://xn--80ak6aa92e.com", ] {
1467 assert!(finder.find(input).is_some(), "{}", input);
1468 }
1469 }
1470
1471 #[test]
1472 fn it_should_handle_edge_case_ports() {
1473 let finder = URI::default();
1474
1475 assert!(finder.find("http://example.com:").is_some());
1477
1478 assert!(finder.find("http://example.com:0").is_some());
1480
1481 assert!(finder.find("http://example.com:99999").is_some());
1483 }
1484
1485 #[test]
1486 fn it_should_handle_edge_case_userinfo() {
1487 let finder = URI::default();
1488
1489 assert!(finder.find("http://@example.com").is_some());
1491
1492 assert!(finder.find("http://user@example.com").is_some());
1494
1495 assert!(finder.find("http://user:@example.com").is_some());
1497
1498 assert!(finder.find("http://:pass@example.com").is_some());
1500
1501 assert!(finder.find("http://user%40name@example.com").is_some());
1503 }
1504
1505 #[test]
1506 fn it_should_handle_consecutive_special_chars() {
1507 let finder = URI::default();
1508
1509 assert!(finder.find("http://example.com?a=1&b=2&c=3").is_some());
1511
1512 let input = "http://example.com#frag#ment";
1514 assert_eq!(
1516 Some("http://example.com#frag"),
1517 finder.find(input).map(|r| &input[r])
1518 );
1519
1520 assert!(finder.find("http://example.com?query#fragment").is_some());
1522 }
1523
1524 #[test]
1525 fn it_should_handle_scheme_edge_cases() {
1526 let finder = URI::default();
1527
1528 assert!(finder.find("h2c://example.com").is_some());
1530
1531 assert!(finder.find("coap+tcp://example.com").is_some());
1533
1534 assert!(finder.find("x.y.z://example.com").is_some());
1536
1537 assert!(finder.find("my-scheme://example.com").is_some());
1539
1540 assert!(finder.find("x://example.com").is_some());
1542 }
1543
1544 #[test]
1545 fn it_should_handle_ipv4_edge_cases() {
1546 let finder = URI::default();
1547
1548 assert!(finder.find("http://0.0.0.0").is_some());
1550 assert!(finder.find("http://255.255.255.255").is_some());
1551
1552 let input = "http://256.0.0.1";
1554 assert!(finder.find(input).is_some()); assert!(finder.find("http://192.168.001.001").is_some());
1558 }
1559
1560 #[test]
1561 fn it_should_handle_multiple_uris_in_text() {
1562 let finder = URI::default();
1563
1564 let text = "Visit http://first.com and https://second.com today";
1565 let result = finder.find(text);
1566 assert!(result.is_some());
1567 assert_eq!("http://first.com", &text[result.unwrap()]);
1568 }
1569
1570 #[test]
1571 fn it_should_handle_uri_at_string_boundaries() {
1572 let finder = URI::default();
1573
1574 let input1 = "http://example.com is a URL";
1576 assert_eq!(
1577 Some("http://example.com"),
1578 finder.find(input1).map(|r| &input1[r])
1579 );
1580
1581 let input2 = "Visit http://example.com";
1583 assert_eq!(
1584 Some("http://example.com"),
1585 finder.find(input2).map(|r| &input2[r])
1586 );
1587
1588 let input3 = "http://example.com";
1590 assert_eq!(
1591 Some("http://example.com"),
1592 finder.find(input3).map(|r| &input3[r])
1593 );
1594 }
1595
1596 #[test]
1597 fn it_should_filter_by_scheme() {
1598 let mut finder = URI::default();
1599 finder.add_scheme("https");
1600
1601 let input1 = "https://secure.com";
1603 assert_eq!(Some(input1), finder.find(input1).map(|r| &input1[r]));
1604
1605 let input2 = "http://insecure.com";
1607 assert_eq!(None, finder.find(input2));
1608
1609 let input3 = "http://first.com https://second.com";
1611 assert_eq!(
1612 Some("https://second.com"),
1613 finder.find(input3).map(|r| &input3[r])
1614 );
1615 }
1616
1617 #[test]
1618 fn it_should_handle_scheme_case_insensitivity() {
1619 let mut finder = URI::default();
1620 finder.add_scheme("HTTP");
1621
1622 let input = "http://example.com";
1624 assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
1625
1626 let input = "HTTP://example.com";
1628 assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
1629 }
1630
1631 #[test]
1632 fn uppercase_http_scheme_obeys_host_rules() {
1633 let finder = URI::default();
1634 assert_eq!(None, finder.find("HTTP:///missing-host"));
1635 assert_eq!(None, finder.find("HTTPS:///missing-host"));
1636 }
1637
1638 #[test]
1641 fn add_scheme_deduplicates() {
1642 let mut finder = URI::default();
1643 finder.add_scheme("http");
1644 finder.add_scheme("http");
1645 finder.add_scheme("HTTP");
1646 assert_eq!(finder.schemes.len(), 1);
1647 }
1648
1649 #[test]
1650 fn add_scheme_multiple_sorted() {
1651 let mut finder = URI::default();
1652 finder.add_scheme("https");
1653 finder.add_scheme("ftp");
1654 finder.add_scheme("http");
1655 assert_eq!(finder.schemes, vec!["ftp", "http", "https"]);
1656 }
1657
1658 #[test]
1661 fn look_pchar_unreserved_chars() {
1662 let finder = URI::default();
1663 for input in [
1664 "http://example.com/path-with-dashes_and_underscores.and.dots~tilde",
1665 "http://example.com/ABCDEFGHIJKLMNOPQRSTUVWXYZ",
1666 "http://example.com/abcdefghijklmnopqrstuvwxyz",
1667 "http://example.com/0123456789",
1668 ] {
1669 assert_eq!(
1670 Some(input),
1671 finder.find(input).map(|r| &input[r]),
1672 "{}",
1673 input
1674 );
1675 }
1676 }
1677
1678 #[test]
1679 fn look_pchar_pct_encoded() {
1680 let finder = URI::default();
1681 let input = "http://example.com/%20%2F%3A";
1682 assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
1683 }
1684
1685 #[test]
1686 fn look_pchar_sub_delims() {
1687 let finder = URI::default();
1688 let input = "http://example.com/path!$&(*+,;=";
1689 assert_eq!(Some(input), finder.find(input).map(|r| &input[r]));
1690 }
1691
1692 #[test]
1695 fn rlook_scheme_very_long_prefix() {
1696 let finder = URI::default();
1697 let long_prefix = " ".repeat(1000);
1698 let input = format!("{}http://example.com", long_prefix);
1699 assert_eq!(
1700 Some("http://example.com"),
1701 finder.find(&input).map(|r| &input[r])
1702 );
1703 }
1704
1705 #[test]
1706 fn rlook_scheme_just_within_cap() {
1707 let finder = URI::default();
1708 let scheme = "a".repeat(30);
1709 let input = format!("{}://host", scheme);
1710 assert_eq!(Some(input.as_str()), finder.find(&input).map(|r| &input[r]));
1711 }
1712
1713 #[test]
1714 fn registered_schemes_should_fit_the_scheme_lookback() {
1715 for scheme in crate::iana::URI_SCHEMES.iter() {
1716 assert!(scheme.len() <= MAX_SCHEME_LEN, "{scheme}");
1717 }
1718 }
1719
1720 #[test]
1721 fn find_should_keep_long_registered_opaque_uris() {
1722 let finder = URI::default();
1723 let input = "open microsoft.windows.camera.multipicker:photo now";
1724 assert_eq!(
1725 Some("microsoft.windows.camera.multipicker:photo"),
1726 finder.find(input).map(|r| &input[r])
1727 );
1728 }
1729
1730 #[test]
1731 fn rlook_scheme_exceeds_cap() {
1732 let finder = URI::default();
1733 let scheme = "a".repeat(MAX_SCHEME_LEN + 8);
1734 let input = format!("{}://host", scheme);
1735 let result = finder.find(&input);
1738 assert!(result.is_some());
1739 }
1740
1741 #[test]
1744 fn sub_delim_strict_vs_lax() {
1745 let strict = {
1746 let mut f = URI::default();
1747 f.strict = true;
1748 f
1749 };
1750 let lax = URI::default();
1751
1752 let with_apos = "http://example.com/it's";
1753 assert_eq!(
1754 Some("http://example.com/it's"),
1755 strict.find(with_apos).map(|r| &with_apos[r])
1756 );
1757 assert_eq!(
1758 Some("http://example.com/it"),
1759 lax.find(with_apos).map(|r| &with_apos[r])
1760 );
1761
1762 let with_paren = "http://example.com/path)";
1763 assert_eq!(
1764 Some("http://example.com/path)"),
1765 strict.find(with_paren).map(|r| &with_paren[r])
1766 );
1767 assert_eq!(
1768 Some("http://example.com/path"),
1769 lax.find(with_paren).map(|r| &with_paren[r])
1770 );
1771 }
1772}