Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
authors = ["Jacob Quinn", "Sam O'Connor", "contributors: https://github.com/JuliaWeb/URIs.jl/graphs/contributors"]
version = "1.7.0"

[deps]
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[compat]
Serialization = "1"
julia = "1.6"

[extras]
Expand Down
47 changes: 47 additions & 0 deletions src/URIs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export URI,
resolvereference

import Base.==
import Serialization

_is_forbidden_ascii(c::Char) = c <= ' ' || c == '\x7f'

Expand Down Expand Up @@ -381,6 +382,52 @@ Base.print(io::IO, u::URI) = print(io, string(u))

Base.string(u::URI) = u.uri === nostring ? uristring(u) : u.uri

# `URI` uses a sentinel `SubString` (`absent`) to distinguish an absent
# component from an empty one, identified by object identity (`===`).
# The default `Serialization` machinery round-trips field values but not
# object identity, so a deserialized URI's components would no longer be
# `===` to `absent`, corrupting the URI (see JuliaWeb/URIs.jl#75). Instead,
# serialize/deserialize through the URI string itself.
#
# The payload is prefixed with an `Int` format marker so that streams written by
# URIs.jl <= 1.7.0, which start with the eight struct fields (the first being
# `uri::String`), remain readable: their first value can never be an `Int`.
const SERIALIZATION_VERSION = 1

function Serialization.serialize(s::Serialization.AbstractSerializer, uri::URI)
Serialization.serialize_type(s, URI)
Serialization.serialize(s, SERIALIZATION_VERSION)
Serialization.serialize(s, string(uri))
end

function Serialization.deserialize(s::Serialization.AbstractSerializer, ::Type{URI})
marker = Serialization.deserialize(s)
if marker isa Integer
marker == SERIALIZATION_VERSION || throw(ErrorException(
"unsupported URI serialization format $marker, expected at most $SERIALIZATION_VERSION; a newer version of URIs.jl is required to read this stream"))
return URI(Serialization.deserialize(s)::AbstractString)
end
# Legacy format: the eight struct fields, in order. All eight must be read
# to leave the stream aligned for whatever follows the URI. The `absent`
# sentinel identity does not survive such a stream, so restore it from
# emptiness, which is what an empty component meant in all but the rare
# present-but-empty case.
uri = marker::String
scheme = _deserialize_component(s)
userinfo = _deserialize_component(s)
host = _deserialize_component(s)
port = _deserialize_component(s)
path = _deserialize_component(s)
query = _deserialize_component(s)
fragment = _deserialize_component(s)
return URI(uri, scheme, userinfo, host, port, path, query, fragment)
end

function _deserialize_component(s::Serialization.AbstractSerializer)
component = Serialization.deserialize(s)::SubString{String}
return isempty(component) ? absent : component
end

#isabsent(ui) = isempty(ui) && !(ui === blank)
isabsent(ui) = ui === absent

Expand Down
62 changes: 62 additions & 0 deletions test/uri.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Test
using Serialization

mutable struct URLTest
name::String
Expand Down Expand Up @@ -742,6 +743,67 @@ urltests = URLTest[
@test_throws AssertionError("foo() failed to ensure `y > 10`\ny = 1\n10 = 10") foo(11, 1)
end

@testset "Serialization roundtrip" begin
# https://github.com/JuliaWeb/URIs.jl/issues/75
for uri in (
URI(; path="some/relative/path.pdf", scheme="file"),
URI("http://user:pass@example.com:8080/path?query=1#frag"),
URI("file:some/relative/path.pdf"),
URI(""),
URI("relative/path"),
URI("mailto:foo@example.com"),
)
buf = IOBuffer()
serialize(buf, uri)
seekstart(buf)
uri2 = deserialize(buf)
@test uri2 isa URI
@test uri == uri2
@test string(uri) == string(uri2)
end
end

@testset "Serialization backwards compatibility" begin
# Emulate the format written by URIs.jl <= 1.7.0, where the default
# `Serialization` machinery wrote the eight struct fields in order.
function serialize_legacy(s::Serialization.AbstractSerializer, uri::URI)
Serialization.serialize_type(s, URI)
for i in 1:nfields(uri)
serialize(s, getfield(uri, i))
end
end

for uri in (
URI(; path="some/relative/path.pdf", scheme="file"),
URI("http://user:pass@example.com:8080/path?query=1#frag"),
URI("file:some/relative/path.pdf"),
URI(""),
URI("relative/path"),
URI("mailto:foo@example.com"),
)
buf = IOBuffer()
s = Serializer(buf)
serialize_legacy(s, uri)
serialize(s, 42)
seekstart(buf)
d = Serializer(buf)
uri2 = deserialize(d)
@test uri2 isa URI
@test uri == uri2
@test string(uri) == string(uri2)
# The stream stays aligned for whatever followed the URI.
@test deserialize(d) == 42
end

# A stream written by a future, unknown format is reported as such.
buf = IOBuffer()
s = Serializer(buf)
Serialization.serialize_type(s, URI)
serialize(s, URIs.SERIALIZATION_VERSION + 1)
seekstart(buf)
@test_throws ErrorException deserialize(Serializer(buf))
end

@testset "download" begin
f_base = download("https://httpbingo.julialang.org/get")
f_uri = download(URI("https://httpbingo.julialang.org/get"))
Expand Down
Loading