1use std::borrow::Cow;
40use std::collections::HashMap;
41use std::fmt;
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct NormalizedIri {
49 iri: String,
51}
52
53impl NormalizedIri {
54 pub fn new_unchecked(iri: String) -> Self {
59 Self { iri }
60 }
61
62 pub fn as_str(&self) -> &str {
64 &self.iri
65 }
66
67 pub fn into_string(self) -> String {
69 self.iri
70 }
71
72 pub fn is_equivalent(&self, other: &Self) -> bool {
74 self.iri == other.iri
75 }
76}
77
78impl fmt::Display for NormalizedIri {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "{}", self.iri)
81 }
82}
83
84impl AsRef<str> for NormalizedIri {
85 fn as_ref(&self) -> &str {
86 &self.iri
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum NormalizationError {
93 InvalidFormat(String),
95 InvalidPercentEncoding(String),
97 MissingScheme,
99 InvalidScheme(String),
101}
102
103impl fmt::Display for NormalizationError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::InvalidFormat(msg) => write!(f, "Invalid IRI format: {}", msg),
107 Self::InvalidPercentEncoding(seq) => {
108 write!(f, "Invalid percent encoding: {}", seq)
109 }
110 Self::MissingScheme => write!(f, "IRI must have a scheme"),
111 Self::InvalidScheme(s) => write!(f, "Invalid scheme: {}", s),
112 }
113 }
114}
115
116impl std::error::Error for NormalizationError {}
117
118pub type NormalizationResult<T> = Result<T, NormalizationError>;
120
121pub fn normalize_iri(iri: &str) -> NormalizationResult<NormalizedIri> {
138 if iri.is_empty() {
139 return Err(NormalizationError::InvalidFormat(
140 "IRI cannot be empty".to_string(),
141 ));
142 }
143
144 let components = parse_iri_components(iri)?;
146
147 let normalized = normalize_components(&components)?;
149
150 Ok(NormalizedIri::new_unchecked(normalized))
151}
152
153#[derive(Debug, Clone)]
155struct IriComponents {
156 scheme: String,
157 authority: Option<Authority>,
158 path: String,
159 query: Option<String>,
160 fragment: Option<String>,
161}
162
163#[derive(Debug, Clone)]
165struct Authority {
166 userinfo: Option<String>,
167 host: String,
168 port: Option<u16>,
169}
170
171fn parse_iri_components(iri: &str) -> NormalizationResult<IriComponents> {
173 let colon_pos = iri.find(':').ok_or(NormalizationError::MissingScheme)?;
175 let scheme = iri[..colon_pos].to_string();
176
177 if scheme.is_empty() || !is_valid_scheme(&scheme) {
178 return Err(NormalizationError::InvalidScheme(scheme));
179 }
180
181 let rest = &iri[colon_pos + 1..];
182
183 let (authority, path_query_fragment) = if let Some(after_slashes) = rest.strip_prefix("//") {
185 let auth_end = after_slashes
186 .find(['/', '?', '#'])
187 .unwrap_or(after_slashes.len());
188 let authority_str = &after_slashes[..auth_end];
189 let authority = parse_authority(authority_str)?;
190 (Some(authority), &after_slashes[auth_end..])
191 } else {
192 (None, rest)
193 };
194
195 let (path, query_fragment) = if let Some(q_pos) = path_query_fragment.find('?') {
197 (
198 path_query_fragment[..q_pos].to_string(),
199 &path_query_fragment[q_pos + 1..],
200 )
201 } else if let Some(f_pos) = path_query_fragment.find('#') {
202 (
203 path_query_fragment[..f_pos].to_string(),
204 &path_query_fragment[f_pos..],
205 )
206 } else {
207 (path_query_fragment.to_string(), "")
208 };
209
210 let (query, fragment) = if !query_fragment.is_empty() {
211 if let Some(f_pos) = query_fragment.find('#') {
212 (
213 Some(query_fragment[..f_pos].to_string()),
214 Some(query_fragment[f_pos + 1..].to_string()),
215 )
216 } else {
217 (Some(query_fragment.to_string()), None)
218 }
219 } else {
220 (None, None)
221 };
222
223 Ok(IriComponents {
224 scheme,
225 authority,
226 path,
227 query,
228 fragment,
229 })
230}
231
232fn parse_authority(authority: &str) -> NormalizationResult<Authority> {
234 if authority.is_empty() {
235 return Ok(Authority {
236 userinfo: None,
237 host: String::new(),
238 port: None,
239 });
240 }
241
242 let (userinfo, host_port) = if let Some(at_pos) = authority.rfind('@') {
244 (
245 Some(authority[..at_pos].to_string()),
246 &authority[at_pos + 1..],
247 )
248 } else {
249 (None, authority)
250 };
251
252 let (host, port) = parse_host_port(host_port)?;
254
255 Ok(Authority {
256 userinfo,
257 host,
258 port,
259 })
260}
261
262fn parse_host_port(host_port: &str) -> NormalizationResult<(String, Option<u16>)> {
264 if let Some(bracket_start) = host_port.find('[') {
266 let bracket_end = host_port.find(']').ok_or_else(|| {
267 NormalizationError::InvalidFormat("Unclosed IPv6 bracket".to_string())
268 })?;
269 let host = host_port[bracket_start..=bracket_end].to_string();
270 let rest = &host_port[bracket_end + 1..];
271 let port = if let Some(port_str) = rest.strip_prefix(':') {
272 Some(port_str.parse::<u16>().map_err(|_| {
273 NormalizationError::InvalidFormat(format!("Invalid port: {}", port_str))
274 })?)
275 } else if rest.is_empty() {
276 None
277 } else {
278 return Err(NormalizationError::InvalidFormat(
279 "Invalid characters after IPv6 address".to_string(),
280 ));
281 };
282 return Ok((host, port));
283 }
284
285 if let Some(colon_pos) = host_port.rfind(':') {
287 let potential_port = &host_port[colon_pos + 1..];
288 if potential_port.chars().all(|c| c.is_ascii_digit()) {
290 let port = potential_port.parse::<u16>().map_err(|_| {
291 NormalizationError::InvalidFormat(format!("Invalid port: {}", potential_port))
292 })?;
293 Ok((host_port[..colon_pos].to_string(), Some(port)))
294 } else {
295 Ok((host_port.to_string(), None))
296 }
297 } else {
298 Ok((host_port.to_string(), None))
299 }
300}
301
302fn normalize_components(components: &IriComponents) -> NormalizationResult<String> {
304 let scheme = components.scheme.to_lowercase();
306
307 let authority_str = if let Some(ref auth) = components.authority {
309 let mut parts = Vec::new();
310
311 if let Some(ref userinfo) = auth.userinfo {
313 let normalized_userinfo = normalize_percent_encoding(userinfo)?;
314 parts.push(format!("{}@", normalized_userinfo));
315 }
316
317 let normalized_host = if auth.host.starts_with('[') {
319 auth.host.to_lowercase()
321 } else {
322 normalize_percent_encoding(&auth.host.to_lowercase())?
324 };
325 parts.push(normalized_host);
326
327 if let Some(port) = auth.port {
329 if !is_default_port(&scheme, port) {
330 parts.push(format!(":{}", port));
331 }
332 }
333
334 format!("//{}", parts.concat())
335 } else {
336 String::new()
337 };
338
339 let normalized_path = normalize_path(&components.path, components.authority.is_some())?;
341
342 let query_str = if let Some(ref query) = components.query {
344 format!("?{}", normalize_percent_encoding(query)?)
345 } else {
346 String::new()
347 };
348
349 let fragment_str = if let Some(ref fragment) = components.fragment {
351 format!("#{}", normalize_percent_encoding(fragment)?)
352 } else {
353 String::new()
354 };
355
356 Ok(format!(
358 "{}:{}{}{}{}",
359 scheme, authority_str, normalized_path, query_str, fragment_str
360 ))
361}
362
363fn normalize_percent_encoding(s: &str) -> NormalizationResult<String> {
368 let mut result = String::with_capacity(s.len());
369 let mut chars = s.chars().peekable();
370
371 while let Some(ch) = chars.next() {
372 if ch == '%' {
373 let hex1 = chars
375 .next()
376 .ok_or_else(|| NormalizationError::InvalidPercentEncoding(format!("%{}", s)))?;
377 let hex2 = chars.next().ok_or_else(|| {
378 NormalizationError::InvalidPercentEncoding(format!("%{}{}", hex1, s))
379 })?;
380
381 let hex_str = format!("{}{}", hex1, hex2);
382 let byte = u8::from_str_radix(&hex_str, 16)
383 .map_err(|_| NormalizationError::InvalidPercentEncoding(format!("%{}", hex_str)))?;
384
385 let decoded = byte as char;
387 if is_unreserved(decoded) {
388 result.push(decoded);
390 } else {
391 result.push_str(&format!("%{}", hex_str.to_uppercase()));
393 }
394 } else {
395 result.push(ch);
396 }
397 }
398
399 Ok(result)
400}
401
402fn is_unreserved(ch: char) -> bool {
404 ch.is_ascii_alphanumeric() || ch == '-' || ch == '.' || ch == '_' || ch == '~'
405}
406
407fn normalize_path(path: &str, has_authority: bool) -> NormalizationResult<String> {
409 if path.is_empty() && has_authority {
411 return Ok("/".to_string());
412 }
413
414 let normalized = remove_dot_segments(path);
416
417 normalize_percent_encoding(&normalized)
419}
420
421fn remove_dot_segments(path: &str) -> String {
423 let mut output = Vec::new();
424 let segments: Vec<&str> = path.split('/').collect();
425 let has_trailing_slash = path.ends_with('/') && path.len() > 1;
426
427 for (i, segment) in segments.iter().enumerate() {
428 match *segment {
429 "" => {
430 if i == 0 {
432 }
434 }
435 "." => {
436 }
438 ".." => {
439 output.pop();
441 }
442 _ => {
443 output.push(*segment);
445 }
446 }
447 }
448
449 if path.starts_with('/') {
451 if output.is_empty() {
452 "/".to_string()
453 } else {
454 let base_path = format!("/{}", output.join("/"));
455 if has_trailing_slash {
456 format!("{}/", base_path)
457 } else {
458 base_path
459 }
460 }
461 } else if output.is_empty() {
462 String::new()
463 } else {
464 let base_path = output.join("/");
465 if has_trailing_slash {
466 format!("{}/", base_path)
467 } else {
468 base_path
469 }
470 }
471}
472
473fn is_default_port(scheme: &str, port: u16) -> bool {
475 get_default_port(scheme) == Some(port)
476}
477
478fn get_default_port(scheme: &str) -> Option<u16> {
480 DEFAULT_PORTS.get(scheme).copied()
481}
482
483static DEFAULT_PORTS: once_cell::sync::Lazy<HashMap<&'static str, u16>> =
485 once_cell::sync::Lazy::new(|| {
486 let mut m = HashMap::new();
487 m.insert("http", 80);
488 m.insert("https", 443);
489 m.insert("ftp", 21);
490 m.insert("ftps", 990);
491 m.insert("ssh", 22);
492 m.insert("telnet", 23);
493 m.insert("smtp", 25);
494 m.insert("pop3", 110);
495 m.insert("imap", 143);
496 m.insert("ldap", 389);
497 m.insert("ldaps", 636);
498 m.insert("ws", 80);
499 m.insert("wss", 443);
500 m
501 });
502
503fn is_valid_scheme(scheme: &str) -> bool {
505 if scheme.is_empty() {
506 return false;
507 }
508 let mut chars = scheme.chars();
509
510 let first = chars.next().expect("iterator should have next element");
512 if !first.is_ascii_alphabetic() {
513 return false;
514 }
515
516 chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
518}
519
520struct ReferenceComponents {
523 scheme: Option<String>,
524 authority: Option<String>,
525 path: String,
526 query: Option<String>,
527 fragment: Option<String>,
528}
529
530fn split_generic_reference(reference: &str) -> ReferenceComponents {
533 let (before_fragment, fragment) = match reference.find('#') {
534 Some(i) => (&reference[..i], Some(reference[i + 1..].to_string())),
535 None => (reference, None),
536 };
537
538 let (before_query, query) = match before_fragment.find('?') {
539 Some(i) => (
540 &before_fragment[..i],
541 Some(before_fragment[i + 1..].to_string()),
542 ),
543 None => (before_fragment, None),
544 };
545
546 let scheme_end = before_query.find(':').filter(|&i| {
551 let candidate = &before_query[..i];
552 !candidate.is_empty()
553 && candidate
554 .chars()
555 .next()
556 .is_some_and(|c| c.is_ascii_alphabetic())
557 && candidate
558 .chars()
559 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
560 });
561
562 let (scheme, rest) = match scheme_end {
563 Some(i) => (Some(before_query[..i].to_string()), &before_query[i + 1..]),
564 None => (None, before_query),
565 };
566
567 let (authority, path) = if let Some(after_slashes) = rest.strip_prefix("//") {
568 let end = after_slashes.find('/').unwrap_or(after_slashes.len());
569 (
570 Some(after_slashes[..end].to_string()),
571 after_slashes[end..].to_string(),
572 )
573 } else {
574 (None, rest.to_string())
575 };
576
577 ReferenceComponents {
578 scheme,
579 authority,
580 path,
581 query,
582 fragment,
583 }
584}
585
586fn recompose(components: &ReferenceComponents) -> String {
588 let mut result = String::new();
589 if let Some(scheme) = &components.scheme {
590 result.push_str(scheme);
591 result.push(':');
592 }
593 if let Some(authority) = &components.authority {
594 result.push_str("//");
595 result.push_str(authority);
596 }
597 result.push_str(&components.path);
598 if let Some(query) = &components.query {
599 result.push('?');
600 result.push_str(query);
601 }
602 if let Some(fragment) = &components.fragment {
603 result.push('#');
604 result.push_str(fragment);
605 }
606 result
607}
608
609fn merge_paths(base_has_authority: bool, base_path: &str, ref_path: &str) -> String {
611 if base_has_authority && base_path.is_empty() {
612 format!("/{ref_path}")
613 } else {
614 match base_path.rfind('/') {
615 Some(i) => format!("{}{}", &base_path[..=i], ref_path),
616 None => ref_path.to_string(),
617 }
618 }
619}
620
621fn remove_dot_segments_rfc3986(path: &str) -> String {
629 fn remove_last_output_segment(output: &mut String) {
630 match output.rfind('/') {
631 Some(pos) => output.truncate(pos),
632 None => output.clear(),
633 }
634 }
635
636 let mut input = path;
637 let mut output = String::new();
638
639 while !input.is_empty() {
640 if let Some(rest) = input.strip_prefix("../") {
641 input = rest;
642 } else if let Some(rest) = input.strip_prefix("./") {
643 input = rest;
644 } else if input.starts_with("/./") {
645 input = &input[2..];
647 } else if input == "/." {
648 input = "/";
649 } else if input.starts_with("/../") {
650 input = &input[3..];
652 remove_last_output_segment(&mut output);
653 } else if input == "/.." {
654 input = "/";
655 remove_last_output_segment(&mut output);
656 } else if input == "." || input == ".." {
657 input = "";
658 } else {
659 let start = usize::from(input.starts_with('/'));
662 let end = input[start..]
663 .find('/')
664 .map(|i| i + start)
665 .unwrap_or(input.len());
666 output.push_str(&input[..end]);
667 input = &input[end..];
668 }
669 }
670
671 output
672}
673
674pub fn resolve_reference(base: &str, reference: &str) -> String {
704 let ref_components = split_generic_reference(reference);
705
706 if ref_components.scheme.is_some() {
707 return recompose(&ReferenceComponents {
708 scheme: ref_components.scheme,
709 authority: ref_components.authority,
710 path: remove_dot_segments_rfc3986(&ref_components.path),
711 query: ref_components.query,
712 fragment: ref_components.fragment,
713 });
714 }
715
716 let base_components = split_generic_reference(base);
717
718 let (t_authority, t_path, t_query) = if ref_components.authority.is_some() {
719 (
720 ref_components.authority,
721 remove_dot_segments_rfc3986(&ref_components.path),
722 ref_components.query,
723 )
724 } else if ref_components.path.is_empty() {
725 (
726 base_components.authority,
727 base_components.path,
728 ref_components.query.or(base_components.query),
729 )
730 } else if ref_components.path.starts_with('/') {
731 (
732 base_components.authority,
733 remove_dot_segments_rfc3986(&ref_components.path),
734 ref_components.query,
735 )
736 } else {
737 let merged = merge_paths(
738 base_components.authority.is_some(),
739 &base_components.path,
740 &ref_components.path,
741 );
742 (
743 base_components.authority,
744 remove_dot_segments_rfc3986(&merged),
745 ref_components.query,
746 )
747 };
748
749 recompose(&ReferenceComponents {
750 scheme: base_components.scheme,
751 authority: t_authority,
752 path: t_path,
753 query: t_query,
754 fragment: ref_components.fragment,
755 })
756}
757
758pub fn iris_equivalent(iri1: &str, iri2: &str) -> NormalizationResult<bool> {
784 let normalized1 = normalize_iri(iri1)?;
785 let normalized2 = normalize_iri(iri2)?;
786 Ok(normalized1.is_equivalent(&normalized2))
787}
788
789pub fn normalize_iri_cow(iri: &str) -> NormalizationResult<Cow<'_, str>> {
793 let normalized = normalize_iri(iri)?;
794 if normalized.as_str() == iri {
795 Ok(Cow::Borrowed(iri))
796 } else {
797 Ok(Cow::Owned(normalized.into_string()))
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 #[test]
806 fn test_case_normalization() {
807 let iri = normalize_iri("HTTP://EXAMPLE.ORG/Path").expect("valid IRI");
808 assert_eq!(iri.as_str(), "http://example.org/Path");
809 }
810
811 #[test]
812 fn test_percent_encoding_normalization() {
813 let iri = normalize_iri("http://example.org/%7Euser").expect("valid IRI");
815 assert_eq!(iri.as_str(), "http://example.org/~user");
816
817 let iri = normalize_iri("http://example.org/%41%42%43").expect("valid IRI");
818 assert_eq!(iri.as_str(), "http://example.org/ABC");
819
820 let iri = normalize_iri("http://example.org/path%20with%20spaces").expect("valid IRI");
822 assert_eq!(iri.as_str(), "http://example.org/path%20with%20spaces");
823 }
824
825 #[test]
826 fn test_default_port_removal() {
827 let iri = normalize_iri("http://example.org:80/path").expect("valid IRI");
828 assert_eq!(iri.as_str(), "http://example.org/path");
829
830 let iri = normalize_iri("https://example.org:443/path").expect("valid IRI");
831 assert_eq!(iri.as_str(), "https://example.org/path");
832
833 let iri = normalize_iri("http://example.org:8080/path").expect("valid IRI");
835 assert_eq!(iri.as_str(), "http://example.org:8080/path");
836 }
837
838 #[test]
839 fn test_path_normalization() {
840 let iri = normalize_iri("http://example.org/a/./b/../c").expect("valid IRI");
841 assert_eq!(iri.as_str(), "http://example.org/a/c");
842
843 let iri = normalize_iri("http://example.org/./a/b").expect("valid IRI");
844 assert_eq!(iri.as_str(), "http://example.org/a/b");
845
846 let iri = normalize_iri("http://example.org/a/b/..").expect("valid IRI");
847 assert_eq!(iri.as_str(), "http://example.org/a");
848 }
849
850 #[test]
851 fn test_empty_path_normalization() {
852 let iri = normalize_iri("http://example.org").expect("valid IRI");
853 assert_eq!(iri.as_str(), "http://example.org/");
854 }
855
856 #[test]
857 fn test_query_and_fragment() {
858 let iri = normalize_iri("http://example.org/path?query=value#fragment").expect("valid IRI");
859 assert_eq!(iri.as_str(), "http://example.org/path?query=value#fragment");
860
861 let iri = normalize_iri("http://example.org/path?q=%41#%42").expect("valid IRI");
863 assert_eq!(iri.as_str(), "http://example.org/path?q=A#B");
864 }
865
866 #[test]
867 fn test_ipv6_address() {
868 let iri = normalize_iri("http://[2001:db8::1]/path").expect("valid IRI");
869 assert_eq!(iri.as_str(), "http://[2001:db8::1]/path");
870
871 let iri = normalize_iri("http://[2001:DB8::1]:8080/path").expect("valid IRI");
872 assert_eq!(iri.as_str(), "http://[2001:db8::1]:8080/path");
873 }
874
875 #[test]
876 fn test_userinfo() {
877 let iri = normalize_iri("http://user:pass@example.org/path").expect("valid IRI");
878 assert_eq!(iri.as_str(), "http://user:pass@example.org/path");
879
880 let iri = normalize_iri("http://%41%42%43@example.org/path").expect("valid IRI");
881 assert_eq!(iri.as_str(), "http://ABC@example.org/path");
882 }
883
884 #[test]
885 fn test_iris_equivalent() {
886 assert!(
887 iris_equivalent("HTTP://EXAMPLE.ORG/path", "http://example.org/path")
888 .expect("valid IRI")
889 );
890
891 assert!(
892 iris_equivalent("http://example.org:80/path", "http://example.org/path")
893 .expect("valid IRI")
894 );
895
896 assert!(
897 iris_equivalent("http://example.org/a/./b/../c", "http://example.org/a/c")
898 .expect("valid IRI")
899 );
900
901 assert!(
902 !iris_equivalent("http://example.org/path1", "http://example.org/path2")
903 .expect("valid IRI")
904 );
905 }
906
907 #[test]
908 fn test_complex_normalization() {
909 let iri = normalize_iri("HTTP://USER@EXAMPLE.ORG:80/A/./B/../C/%7Euser?Q=%41#%42")
910 .expect("valid IRI");
911 assert_eq!(iri.as_str(), "http://USER@example.org/A/C/~user?Q=A#B");
912 }
913
914 #[test]
915 fn test_non_http_schemes() {
916 let iri = normalize_iri("ftp://example.org:21/path").expect("valid IRI");
917 assert_eq!(iri.as_str(), "ftp://example.org/path");
918
919 let iri = normalize_iri("urn:isbn:0451450523").expect("valid IRI");
920 assert_eq!(iri.as_str(), "urn:isbn:0451450523");
921 }
922
923 #[test]
924 fn test_invalid_iri() {
925 assert!(normalize_iri("").is_err());
926 assert!(normalize_iri("not an iri").is_err());
927 assert!(normalize_iri("http://example.org/%ZZ").is_err());
928 }
929
930 #[test]
931 fn test_normalized_iri_methods() {
932 let iri1 = normalize_iri("http://example.org/path").expect("valid IRI");
933 let iri2 = normalize_iri("HTTP://EXAMPLE.ORG/path").expect("valid IRI");
934
935 assert_eq!(iri1.as_str(), "http://example.org/path");
936 assert!(iri1.is_equivalent(&iri2));
937 assert_eq!(iri1, iri2);
938
939 let cloned = iri1.clone();
940 assert_eq!(iri1, cloned);
941 }
942
943 #[test]
944 fn test_normalize_iri_cow() {
945 let iri = "http://example.org/path";
947 let result = normalize_iri_cow(iri).expect("valid IRI");
948 assert!(matches!(result, Cow::Borrowed(_)));
949 assert_eq!(result, iri);
950
951 let iri = "HTTP://EXAMPLE.ORG/path";
953 let result = normalize_iri_cow(iri).expect("valid IRI");
954 assert!(matches!(result, Cow::Owned(_)));
955 assert_eq!(result, "http://example.org/path");
956 }
957
958 #[test]
959 fn test_urn_normalization() {
960 let iri = normalize_iri("URN:ISBN:0451450523").expect("valid IRI");
962 assert_eq!(iri.as_str(), "urn:ISBN:0451450523");
963 }
964
965 #[test]
966 fn test_resolve_reference_relative_path_no_trailing_slash_in_base() {
967 assert_eq!(
970 resolve_reference("http://example.org/data", "foo"),
971 "http://example.org/foo"
972 );
973 }
974
975 #[test]
976 fn test_resolve_reference_absolute_path() {
977 assert_eq!(
978 resolve_reference("http://example.org/a/b/c", "/x/y"),
979 "http://example.org/x/y"
980 );
981 }
982
983 #[test]
984 fn test_resolve_reference_dot_segments() {
985 assert_eq!(
986 resolve_reference("http://example.org/a/b/c", "../d"),
987 "http://example.org/a/d"
988 );
989 assert_eq!(
990 resolve_reference("http://a/b/c/d;p?q", "./g"),
991 "http://a/b/c/g"
992 );
993 assert_eq!(
994 resolve_reference("http://a/b/c/d;p?q", "../../../g"),
995 "http://a/g"
996 );
997 }
998
999 #[test]
1000 fn test_resolve_reference_fragment_only() {
1001 assert_eq!(
1002 resolve_reference("http://example.org/a/b#frag1", "#frag2"),
1003 "http://example.org/a/b#frag2"
1004 );
1005 }
1006
1007 #[test]
1008 fn test_resolve_reference_absolute_reference_unchanged() {
1009 assert_eq!(
1010 resolve_reference("http://example.org/a/", "http://other.org/z"),
1011 "http://other.org/z"
1012 );
1013 }
1014
1015 #[test]
1016 fn test_resolve_reference_network_path() {
1017 assert_eq!(
1018 resolve_reference("http://example.org/a/b", "//other.org/z"),
1019 "http://other.org/z"
1020 );
1021 }
1022
1023 #[test]
1024 fn test_resolve_reference_query_only() {
1025 assert_eq!(
1026 resolve_reference("http://example.org/a/b?x=1", "?y=2"),
1027 "http://example.org/a/b?y=2"
1028 );
1029 }
1030
1031 #[test]
1032 fn test_resolve_reference_empty_reference_same_document() {
1033 assert_eq!(
1034 resolve_reference("http://example.org/a/b?x=1#f", ""),
1035 "http://example.org/a/b?x=1"
1036 );
1037 }
1038
1039 #[test]
1040 fn test_trailing_slash() {
1041 let iri1 = normalize_iri("http://example.org/path/").expect("valid IRI");
1042 let iri2 = normalize_iri("http://example.org/path").expect("valid IRI");
1043
1044 assert_ne!(iri1, iri2);
1046 assert_eq!(iri1.as_str(), "http://example.org/path/");
1047 assert_eq!(iri2.as_str(), "http://example.org/path");
1048 }
1049}