diff --git a/stdlib/UUIDs/docs/src/index.md b/stdlib/UUIDs/docs/src/index.md index 3b936e7c82178..4c2f6a5e33c58 100644 --- a/stdlib/UUIDs/docs/src/index.md +++ b/stdlib/UUIDs/docs/src/index.md @@ -7,6 +7,7 @@ DocTestSetup = :(using UUIDs, Random) ```@docs UUIDs.uuid1 UUIDs.uuid4 +UUIDs.uuid5 UUIDs.uuid_version ``` diff --git a/stdlib/UUIDs/src/UUIDs.jl b/stdlib/UUIDs/src/UUIDs.jl index bf618cd6eeff3..6a5e0adb79102 100644 --- a/stdlib/UUIDs/src/UUIDs.jl +++ b/stdlib/UUIDs/src/UUIDs.jl @@ -4,7 +4,9 @@ module UUIDs using Random -export UUID, uuid1, uuid4, uuid_version +import SHA + +export UUID, uuid1, uuid4, uuid5, uuid_version import Base: UUID @@ -22,6 +24,13 @@ julia> uuid_version(uuid4()) """ uuid_version(u::UUID) = Int((u.value >> 76) & 0xf) +# Some UUID namespaces provided in the appendix of RFC 4122 +# https://tools.ietf.org/html/rfc4122.html#appendix-C +const namespace_dns = UUID(0x6ba7b8109dad11d180b400c04fd430c8) # 6ba7b810-9dad-11d1-80b4-00c04fd430c8 +const namespace_url = UUID(0x6ba7b8119dad11d180b400c04fd430c8) # 6ba7b811-9dad-11d1-80b4-00c04fd430c8 +const namespace_oid = UUID(0x6ba7b8129dad11d180b400c04fd430c8) # 6ba7b812-9dad-11d1-80b4-00c04fd430c8 +const namespace_x500 = UUID(0x6ba7b8149dad11d180b400c04fd430c8) # 6ba7b814-9dad-11d1-80b4-00c04fd430c8 + """ uuid1([rng::AbstractRNG=GLOBAL_RNG]) -> UUID @@ -81,4 +90,40 @@ function uuid4(rng::AbstractRNG=Random.GLOBAL_RNG) UUID(u) end +""" + uuid5(ns::UUID, name::String) -> UUID + +Generates a version 5 (namespace and domain-based) universally unique identifier (UUID), +as specified by RFC 4122. + +# Examples +```jldoctest +julia> rng = MersenneTwister(1234); + +julia> u4 = uuid4(rng) +UUID("196f2941-2d58-45ba-9f13-43a2532b2fa8") + +julia> u5 = uuid5(u4, "julia") +UUID("b37756f8-b0c0-54cd-a466-19b3d25683bc") +``` +""" +function uuid5(ns::UUID, name::String) + nsbytes = zeros(UInt8, 16) + nsv = ns.value + for idx in Base.OneTo(16) + nsbytes[idx] = nsv >> 120 + nsv = nsv << 8 + end + hash_result = SHA.sha1(append!(nsbytes, convert(Vector{UInt8}, codeunits(unescape_string(name))))) + # set version number to 5 + hash_result[7] = (hash_result[7] & 0x0F) | (0x50) + hash_result[9] = (hash_result[9] & 0x3F) | (0x80) + v = zero(UInt128) + #use only the first 16 bytes of the SHA1 hash + for idx in Base.OneTo(16) + v = (v << 0x08) | hash_result[idx] + end + return UUID(v) +end + end diff --git a/stdlib/UUIDs/test/runtests.jl b/stdlib/UUIDs/test/runtests.jl index aa8a78307b0c8..8258ac3f27d68 100644 --- a/stdlib/UUIDs/test/runtests.jl +++ b/stdlib/UUIDs/test/runtests.jl @@ -4,13 +4,53 @@ using Test, UUIDs, Random u1 = uuid1() u4 = uuid4() +u5 = uuid5(u1, "julia") @test uuid_version(u1) == 1 @test uuid_version(u4) == 4 +@test uuid_version(u5) == 5 @test u1 == UUID(string(u1)) == UUID(GenericString(string(u1))) @test u4 == UUID(string(u4)) == UUID(GenericString(string(u4))) +@test u5 == UUID(string(u5)) == UUID(GenericString(string(u5))) @test u1 == UUID(UInt128(u1)) @test u4 == UUID(UInt128(u4)) +@test u5 == UUID(UInt128(u5)) @test uuid4(MersenneTwister(0)) == uuid4(MersenneTwister(0)) @test_throws ArgumentError UUID("550e8400e29b-41d4-a716-446655440000") @test_throws ArgumentError UUID("550e8400e29b-41d4-a716-44665544000098") @test_throws ArgumentError UUID("z50e8400-e29b-41d4-a716-446655440000") + +# results similar to Python builtin uuid +# To reproduce the sequence +#= +import uuid +uuids = [uuid.UUID("22b4a8a1-e548-4eeb-9270-60426d66a48e")] +for _ in range(5): + uuids.append(uuid.uuid5(uuids[-1], "julia")) +=# + +const following_uuids = [ + UUID("22b4a8a1-e548-4eeb-9270-60426d66a48e"), + UUID("30ea6cfd-c270-569f-b4cb-795dead63686"), + UUID("31099374-e3a0-5fde-9482-791c639bf29b"), + UUID("6b34b357-a348-53aa-8c71-fb9b06c3a51e"), + UUID("fdbd7d4d-c462-59cc-ae6a-0c3b010240e2"), + UUID("d8cc6298-75d5-57e0-996c-279259ab365c"), +] + +for (idx, init_uuid) in enumerate(following_uuids[1:end-1]) + next_id = uuid5(init_uuid, "julia") + @test next_id == following_uuids[idx+1] +end + +# Python-generated UUID following each of the standard namespaces +const standard_namespace_uuids = [ + (UUIDs.namespace_dns, UUID("00ca23ad-40ef-500c-a910-157de3950d07")), + (UUIDs.namespace_oid, UUID("b7bf72b0-fb4e-538b-952a-3be296f07f6d")), + (UUIDs.namespace_url, UUID("997cd5be-4705-5439-9fe6-d77b18d612e5")), + (UUIDs.namespace_x500, UUID("993c6684-82e7-5cdb-bd46-9bff0362e6a9")), +] + +for (init_uuid, next_uuid) in standard_namespace_uuids + result = uuid5(init_uuid, "julia") + @test next_uuid == result +end