Skip to content

Commit c6f60fe

Browse files
authored
Merge pull request #668 from sdroege/make-relative
Add Url::make_relative() as the inverse of Url::join()
2 parents 77fb472 + d51cec4 commit c6f60fe

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed

url/src/lib.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ impl Url {
320320

321321
/// Parse a string as an URL, with this URL as the base URL.
322322
///
323+
/// The inverse of this is [`make_relative`].
324+
///
323325
/// Note: a trailing slash is significant.
324326
/// Without it, the last path component is considered to be a “file” name
325327
/// to be removed to get at the “directory” that is used as the base:
@@ -349,11 +351,144 @@ impl Url {
349351
/// with this URL as the base URL, a [`ParseError`] variant will be returned.
350352
///
351353
/// [`ParseError`]: enum.ParseError.html
354+
/// [`make_relative`]: #method.make_relative
352355
#[inline]
353356
pub fn join(&self, input: &str) -> Result<Url, crate::ParseError> {
354357
Url::options().base_url(Some(self)).parse(input)
355358
}
356359

360+
/// Creates a relative URL if possible, with this URL as the base URL.
361+
///
362+
/// This is the inverse of [`join`].
363+
///
364+
/// # Examples
365+
///
366+
/// ```rust
367+
/// use url::Url;
368+
/// # use url::ParseError;
369+
///
370+
/// # fn run() -> Result<(), ParseError> {
371+
/// let base = Url::parse("https://example.net/a/b.html")?;
372+
/// let url = Url::parse("https://example.net/a/c.png")?;
373+
/// let relative = base.make_relative(&url);
374+
/// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png"));
375+
///
376+
/// let base = Url::parse("https://example.net/a/b/")?;
377+
/// let url = Url::parse("https://example.net/a/b/c.png")?;
378+
/// let relative = base.make_relative(&url);
379+
/// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png"));
380+
///
381+
/// let base = Url::parse("https://example.net/a/b/")?;
382+
/// let url = Url::parse("https://example.net/a/d/c.png")?;
383+
/// let relative = base.make_relative(&url);
384+
/// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("../d/c.png"));
385+
///
386+
/// let base = Url::parse("https://example.net/a/b.html?c=d")?;
387+
/// let url = Url::parse("https://example.net/a/b.html?e=f")?;
388+
/// let relative = base.make_relative(&url);
389+
/// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("?e=f"));
390+
/// # Ok(())
391+
/// # }
392+
/// # run().unwrap();
393+
/// ```
394+
///
395+
/// # Errors
396+
///
397+
/// If this URL can't be a base for the given URL, `None` is returned.
398+
/// This is for example the case if the scheme, host or port are not the same.
399+
///
400+
/// [`join`]: #method.join
401+
pub fn make_relative(&self, url: &Url) -> Option<String> {
402+
if self.cannot_be_a_base() {
403+
return None;
404+
}
405+
406+
// Scheme, host and port need to be the same
407+
if self.scheme() != url.scheme() || self.host() != url.host() || self.port() != url.port() {
408+
return None;
409+
}
410+
411+
// We ignore username/password at this point
412+
413+
// The path has to be transformed
414+
let mut relative = String::new();
415+
416+
// Extract the filename of both URIs, these need to be handled separately
417+
fn extract_path_filename(s: &str) -> (&str, &str) {
418+
let last_slash_idx = s.rfind('/').unwrap_or(0);
419+
let (path, filename) = s.split_at(last_slash_idx);
420+
if filename.is_empty() {
421+
(path, "")
422+
} else {
423+
(path, &filename[1..])
424+
}
425+
}
426+
427+
let (base_path, base_filename) = extract_path_filename(self.path());
428+
let (url_path, url_filename) = extract_path_filename(url.path());
429+
430+
let mut base_path = base_path.split('/').peekable();
431+
let mut url_path = url_path.split('/').peekable();
432+
433+
// Skip over the common prefix
434+
while base_path.peek().is_some() && base_path.peek() == url_path.peek() {
435+
base_path.next();
436+
url_path.next();
437+
}
438+
439+
// Add `..` segments for the remainder of the base path
440+
for base_path_segment in base_path {
441+
// Skip empty last segments
442+
if base_path_segment.is_empty() {
443+
break;
444+
}
445+
446+
if !relative.is_empty() {
447+
relative.push('/');
448+
}
449+
450+
relative.push_str("..");
451+
}
452+
453+
// Append the remainder of the other URI
454+
for url_path_segment in url_path {
455+
if !relative.is_empty() {
456+
relative.push('/');
457+
}
458+
459+
relative.push_str(url_path_segment);
460+
}
461+
462+
// Add the filename if they are not the same
463+
if base_filename != url_filename {
464+
// If the URIs filename is empty this means that it was a directory
465+
// so we'll have to append a '/'.
466+
//
467+
// Otherwise append it directly as the new filename.
468+
if url_filename.is_empty() {
469+
relative.push('/');
470+
} else {
471+
if !relative.is_empty() {
472+
relative.push('/');
473+
}
474+
relative.push_str(url_filename);
475+
}
476+
}
477+
478+
// Query and fragment are only taken from the other URI
479+
if let Some(query) = url.query() {
480+
relative.push('?');
481+
relative.push_str(query);
482+
}
483+
484+
if let Some(fragment) = url.fragment() {
485+
relative.push('#');
486+
relative.push_str(fragment);
487+
}
488+
489+
Some(relative)
490+
}
491+
357492
/// Return a default `ParseOptions` that can fully configure the URL parser.
358493
///
359494
/// # Examples

url/tests/unit.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,3 +960,127 @@ fn test_slicing() {
960960
assert_eq!(&url[..AfterFragment], expected_slices.full);
961961
}
962962
}
963+
964+
#[test]
965+
fn test_make_relative() {
966+
let tests = [
967+
(
968+
"http://127.0.0.1:8080/test",
969+
"http://127.0.0.1:8080/test",
970+
"",
971+
),
972+
(
973+
"http://127.0.0.1:8080/test",
974+
"http://127.0.0.1:8080/test/",
975+
"test/",
976+
),
977+
(
978+
"http://127.0.0.1:8080/test/",
979+
"http://127.0.0.1:8080/test",
980+
"../test",
981+
),
982+
(
983+
"http://127.0.0.1:8080/",
984+
"http://127.0.0.1:8080/?foo=bar#123",
985+
"?foo=bar#123",
986+
),
987+
(
988+
"http://127.0.0.1:8080/",
989+
"http://127.0.0.1:8080/test/video",
990+
"test/video",
991+
),
992+
(
993+
"http://127.0.0.1:8080/test",
994+
"http://127.0.0.1:8080/test/video",
995+
"test/video",
996+
),
997+
(
998+
"http://127.0.0.1:8080/test/",
999+
"http://127.0.0.1:8080/test/video",
1000+
"video",
1001+
),
1002+
(
1003+
"http://127.0.0.1:8080/test",
1004+
"http://127.0.0.1:8080/test2/video",
1005+
"test2/video",
1006+
),
1007+
(
1008+
"http://127.0.0.1:8080/test/",
1009+
"http://127.0.0.1:8080/test2/video",
1010+
"../test2/video",
1011+
),
1012+
(
1013+
"http://127.0.0.1:8080/test/bla",
1014+
"http://127.0.0.1:8080/test2/video",
1015+
"../test2/video",
1016+
),
1017+
(
1018+
"http://127.0.0.1:8080/test/bla/",
1019+
"http://127.0.0.1:8080/test2/video",
1020+
"../../test2/video",
1021+
),
1022+
(
1023+
"http://127.0.0.1:8080/test/?foo=bar#123",
1024+
"http://127.0.0.1:8080/test/video",
1025+
"video",
1026+
),
1027+
(
1028+
"http://127.0.0.1:8080/test/",
1029+
"http://127.0.0.1:8080/test/video?baz=meh#456",
1030+
"video?baz=meh#456",
1031+
),
1032+
(
1033+
"http://127.0.0.1:8080/test",
1034+
"http://127.0.0.1:8080/test?baz=meh#456",
1035+
"?baz=meh#456",
1036+
),
1037+
(
1038+
"http://127.0.0.1:8080/test/",
1039+
"http://127.0.0.1:8080/test?baz=meh#456",
1040+
"../test?baz=meh#456",
1041+
),
1042+
(
1043+
"http://127.0.0.1:8080/test/",
1044+
"http://127.0.0.1:8080/test/?baz=meh#456",
1045+
"?baz=meh#456",
1046+
),
1047+
(
1048+
"http://127.0.0.1:8080/test/?foo=bar#123",
1049+
"http://127.0.0.1:8080/test/video?baz=meh#456",
1050+
"video?baz=meh#456",
1051+
),
1052+
];
1053+
1054+
for (base, uri, relative) in &tests {
1055+
let base_uri = url::Url::parse(base).unwrap();
1056+
let relative_uri = url::Url::parse(uri).unwrap();
1057+
let make_relative = base_uri.make_relative(&relative_uri).unwrap();
1058+
assert_eq!(
1059+
make_relative, *relative,
1060+
"base: {}, uri: {}, relative: {}",
1061+
base, uri, relative
1062+
);
1063+
assert_eq!(
1064+
base_uri.join(&relative).unwrap().as_str(),
1065+
*uri,
1066+
"base: {}, uri: {}, relative: {}",
1067+
base,
1068+
uri,
1069+
relative
1070+
);
1071+
}
1072+
1073+
let error_tests = [
1074+
("http://127.0.0.1:8080/", "https://127.0.0.1:8080/test/"),
1075+
("http://127.0.0.1:8080/", "http://127.0.0.1:8081/test/"),
1076+
("http://127.0.0.1:8080/", "http://127.0.0.2:8080/test/"),
1077+
("mailto:[email protected]", "mailto:[email protected]"),
1078+
];
1079+
1080+
for (base, uri) in &error_tests {
1081+
let base_uri = url::Url::parse(base).unwrap();
1082+
let relative_uri = url::Url::parse(uri).unwrap();
1083+
let make_relative = base_uri.make_relative(&relative_uri);
1084+
assert_eq!(make_relative, None, "base: {}, uri: {}", base, uri);
1085+
}
1086+
}

0 commit comments

Comments
 (0)