diff --git a/docs/src/index.md b/docs/src/index.md index 3322c89..228ceff 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -57,6 +57,7 @@ absuri escapeuri unescapeuri escapepath +resolvereference URIs.splitpath Base.isvalid(::URI) ``` diff --git a/src/URIs.jl b/src/URIs.jl index 11575f5..2cec0f8 100644 --- a/src/URIs.jl +++ b/src/URIs.jl @@ -2,7 +2,8 @@ module URIs export URI, queryparams, absuri, - escapeuri, unescapeuri, escapepath + escapeuri, unescapeuri, escapepath, + resolvereference import Base.== @@ -523,6 +524,88 @@ function Base.joinpath(uri::URI, parts::String...) return URI(uri; path=normpath(path)) end +""" + resolvereference(base::Union{URI,AbstractString}, ref::Union{URI,AbstractString}) -> URI + +Resolve a URI reference `ref` relative to the absolute base URI `base`, +complying with [RFC 3986 Section 5.2](https://tools.ietf.org/html/rfc3986#section-5.2). + +If `ref` is an absolute URI, return `ref` unchanged. + +# Examples + +```jldoctest; setup = :(using URIs) +julia> u = resolvereference("http://example.org/foo/bar/", "/baz/") +URI("http://example.org/baz/") + +julia> resolvereference(u, "./hello/world") +URI("http://example.org/baz/hello/world") + +julia> resolvereference(u, "http://localhost:8000") +URI("http://localhost:8000") +``` +""" +function resolvereference(base::URI, ref::URI) + # In the case where the second URI is absolute, we just return the + # reference URI. Refer to https://tools.ietf.org/html/rfc3986#section-5.2.2 + # + # We also default to just returning the reference when the base URI is + # non-absolute. + if isempty(base.scheme) || !isempty(ref.scheme) + return ref + end + + host, port, path, query = if !isempty(ref.host) + ref.host, ref.port, ref.path, ref.query + else + path, query = if isempty(ref.path) + base.path, isempty(ref.query) ? base.query : ref.query + else + path = startswith(ref.path, "/") ? ref.path : resolveref_merge(base, ref) + path, ref.query + end + base.host, base.port, path, query + end + + path = normpath(path) + scheme = base.scheme + fragment = ref.fragment + userinfo = isempty(ref.userinfo) ? base.userinfo : ref.userinfo + + URI(; + scheme=scheme, + userinfo=userinfo, + host=host, + port=port, + path=path, + query=query, + fragment=fragment + ) +end + +resolvereference(base, ref) = resolvereference(URI(base), URI(ref)) + +""" + resolveref_merge(base, ref) + +Implementation of the "merge" routine described in RFC 3986 Sec. 5.2.3 for merging +a relative-path reference with the path of the base URI. +""" +function resolveref_merge(base, ref) + if !isempty(base.host) && isempty(base.path) + "/" * ref.path + else + last_slash = findprev("/", base.path, lastindex(base.path)) + if last_slash === nothing + ref.path + else + last_slash = first(last_slash) + base.path[1:last_slash] * ref.path + end + end +end + + function access_threaded(f, v::Vector) tid = Threads.threadid() 0 < tid <= length(v) || _length_assert() diff --git a/test/uri.jl b/test/uri.jl index b863c0a..055cdcb 100644 --- a/test/uri.jl +++ b/test/uri.jl @@ -547,4 +547,55 @@ urltests = URLTest[ @test joinpath(URIs.URI("http://a.b.c/"), "b", "c") == URI("http://a.b.c/b/c") @test joinpath(URIs.URI("http://a.b.c"), "b", "c") == URI("http://a.b.c/b/c") end + + @testset "resolvereference" begin + # Tests for resolving URI references, as defined in Section 5.4 + + # Perform some basic tests resolving absolute and relative references to a base URI + uri = URI("http://example.org/foo/bar/") + @test resolvereference(uri, "/baz") == URI("http://example.org/baz") + @test resolvereference(uri, "baz/") == URI("http://example.org/foo/bar/baz/") + @test resolvereference(uri, "../baz/") == URI("http://example.org/foo/baz/") + + # If the base URI's path doesn't end with a /, we handle relative URIs a little differently + uri = URI("http://example.org/foo/bar") + @test resolvereference(uri, "baz") == URI("http://example.org/foo/baz") + @test resolvereference(uri, "../baz") == URI("http://example.org/baz") + + # If the second URI is absolute, or the first URI isn't, we should just return the + # second URI. + @test resolvereference("http://www.example.org", "http://example.com") == URI("http://example.com") + @test resolvereference("http://example.org/foo", "http://example.org/bar") == URI("http://example.org/bar") + @test resolvereference("/foo", "/bar/baz") == URI("/bar/baz") + + # "Normal examples" specified in Section 5.4.1 + base = URI("http://a/b/c/d;p?q") + @test resolvereference(base, "g:h") == URI("g:h") + @test resolvereference(base, "g") == URI("http://a/b/c/g") + @test resolvereference(base, "./g") == URI("http://a/b/c/g") + @test resolvereference(base, "g/") == URI("http://a/b/c/g/") + @test resolvereference(base, "/g") == URI("http://a/g") + @test resolvereference(base, "//g") == URI("http://g") + @test resolvereference(base, "?y") == URI("http://a/b/c/d;p?y") + @test resolvereference(base, "g?y") == URI("http://a/b/c/g?y") + @test resolvereference(base, "#s") == URI("http://a/b/c/d;p?q#s") + @test resolvereference(base, "g#s") == URI("http://a/b/c/g#s") + @test resolvereference(base, "g?y#s") == URI("http://a/b/c/g?y#s") + @test resolvereference(base, ";x") == URI("http://a/b/c/;x") + @test resolvereference(base, "g;x") == URI("http://a/b/c/g;x") + @test resolvereference(base, "g;x?y#s") == URI("http://a/b/c/g;x?y#s") + @test resolvereference(base, "") == URI("http://a/b/c/d;p?q") + @test resolvereference(base, ".") == URI("http://a/b/c/") + @test resolvereference(base, "./") == URI("http://a/b/c/") + @test resolvereference(base, "..") == URI("http://a/b/") + @test resolvereference(base, "../") == URI("http://a/b/") + @test resolvereference(base, "../g") == URI("http://a/b/g") + @test resolvereference(base, "../..") == URI("http://a/") + @test resolvereference(base, "../../") == URI("http://a/") + @test resolvereference(base, "../../g") == URI("http://a/g") + + # "Abnormal examples" specified in Section 5.4.2 + @test resolvereference(base, "../../../g") == URI("http://a/g") + @test resolvereference(base, "../../../../g") == URI("http://a/g") + end end