Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Add UUID v6 #2

Merged
merged 1 commit into from
Oct 14, 2020
Merged
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ iex> UUID.uuid5("fcfe5f21-8a08-4c9a-9f97-29d2fd6a27b9", "my.domain.com")
"b8e85535-761a-586f-9c04-0fb0df2cbe84"
```

### UUID v6

Generated using a combination of time since the west adopted the gregorian calendar and either the node id MAC address or random bytes.
Valid node types are `:mac_address` or `:random_bytes` and defaults to `:mac_address`.

```elixir
iex> UUID.uuid6()
"1eb0d1d0-126a-6495-9a93-171634969e27"

iex> UUID.uuid6(:random_bytes)
"1eb0d1d5-c3fa-6b2e-8d7a-ef182baf6b94"
```

### Formatting

All UUID generator functions have an optional format parameter as the last argument.
Expand Down
9 changes: 9 additions & 0 deletions bench/uuid_bench.exs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,13 @@ defmodule UUIDBench do
UUID.uuid5(:dns, "test.example.com")
end

bench "uuid6 mac_address" do
UUID.uuid6(:mac_address)
:ok
end

bench "uuid6 random_bytes" do
UUID.uuid6(:random_bytes)
:ok
end
end
116 changes: 116 additions & 0 deletions lib/uuid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule UUID do
@uuid_v3 3 # UUID v3 identifier.
@uuid_v4 4 # UUID v4 identifier.
@uuid_v5 5 # UUID v5 identifier.
@uuid_v6 6 # UUID v6 identifier.

@urn "urn:uuid:" # UUID URN prefix.

Expand Down Expand Up @@ -450,6 +451,114 @@ defmodule UUID do
"Invalid argument; Expected: :dns|:url|:oid|:x500|:nil OR String, String"
end

@doc """
Generate a new UUID v6. This version uses a combination of one or more of:
unix epoch, random bytes, pid hash, and hardware address.

Accepts a `node_type` argument that can be either `:mac_address` or
`:random_bytes`. Defaults to `:mac_address`. However, if there is a security
concern with using a MAC address, use `:random_bytes`.

See the [RFC draft, section 3.3](https://tools.ietf.org/html/draft-peabody-dispatch-new-uuid-format-00#section-3.3)
for more information on the node parts.

## Examples

iex> UUID.uuid6()
"1eb0d28f-da4c-6eb2-adc1-0242ac120002"

iex> UUID.uuid6(:random_bytes, :default)
"1eb0d297-eb1e-62a6-a37f-a55eda5dd6e4"

iex> UUID.uuid6(:random_bytes, :hex)
"1eb0d298502563fcadcd25e5d0a44c1a"

iex> UUID.uuid6(:random_bytes, :urn)
"urn:uuid:1eb0d298-ca10-6914-ab0e-7d7e1e6e1808"

iex> UUID.uuid6(:random_bytes, :raw)
<<30, 176, 210, 153, 52, 23, 102, 230, 164, 146, 99, 66, 4, 72, 220, 114>>

iex> UUID.uuid6(:random_bytes, :slug)
"HrDSmab8ZnqR4SKw4LN-UA"

"""
def uuid6(node_type \\ :mac_address, format \\ :default)
when node_type in [:mac_address, :random_bytes] do
uuid6(uuid1_clockseq(), uuid6_node(node_type), format)
end

@doc """
Generate a new UUID v6, with an existing clock sequence and node address. This
version uses a combination of one or more of: unix epoch, random bytes,
pid hash, and hardware address.
"""
def uuid6(<<clock_seq::14>>, <<node::48>>, format) do
<<time_hi::12, time_mid::16, time_low::32>> = uuid1_time()
<<time_low1::20, time_low2::12>> = <<time_low::32>>
<<clock_seq_hi::6, clock_seq_low::8>> = <<clock_seq::14>>

<<time_hi::12, time_mid::16, time_low1::20, @uuid_v6::4, time_low2::12,
@variant10::2, clock_seq_hi::6, clock_seq_low::8, node::48>>
|> uuid_to_string(format)
end
def uuid6(_, _, _) do
raise ArgumentError, message:
"Invalid argument; Expected: <<clock_seq::14>>, <<node::48>>"
end

@doc """
Convert a UUID v1 to a UUID v6 in the same format.

## Examples

iex> UUID.uuid1_to_uuid6("dafc431a-0d21-11eb-adc1-0242ac120002")
"1eb0d21d-afc4-631a-adc1-0242ac120002"

iex> UUID.uuid1_to_uuid6("2vxDGg0hEeutwQJCrBIAAg")
"HrDSHa_EYxqtwQJCrBIAAg"

iex> UUID.uuid1_to_uuid6(<<218, 252, 67, 26, 13, 33, 17, 235, 173, 193, 2, 66, 172, 18, 0, 2>>)
<<30, 176, 210, 29, 175, 196, 99, 26, 173, 193, 2, 66, 172, 18, 0, 2>>

"""
def uuid1_to_uuid6(uuid1) do
{format, ub1} = uuid_string_to_hex_pair(uuid1)

<<time_low::32, time_mid::16, @uuid_v1::4, time_hi::12, rest::binary>> = ub1
<<time_low1::20, time_low2::12>> = <<time_low::32>>

<<time_hi::12, time_mid::16, time_low1::20, @uuid_v6::4, time_low2::12,
rest::binary>>
|> uuid_to_string(format)
end

@doc """
Convert a UUID v6 to a UUID v1 in the same format.

## Examples

iex> UUID.uuid6_to_uuid1("1eb0d21d-afc4-631a-adc1-0242ac120002")
"dafc431a-0d21-11eb-adc1-0242ac120002"

iex> UUID.uuid6_to_uuid1("HrDSHa_EYxqtwQJCrBIAAg")
"2vxDGg0hEeutwQJCrBIAAg"

iex> UUID.uuid6_to_uuid1(<<30, 176, 210, 29, 175, 196, 99, 26, 173, 193, 2, 66, 172, 18, 0, 2>>)
<<218, 252, 67, 26, 13, 33, 17, 235, 173, 193, 2, 66, 172, 18, 0, 2>>

"""
def uuid6_to_uuid1(uuid6) do
{format, ub6} = uuid_string_to_hex_pair(uuid6)

<<time_hi::12, time_mid::16, time_low1::20, @uuid_v6::4, time_low2::12,
rest::binary>> = ub6
<<time_low::32>> = <<time_low1::20, time_low2::12>>

<<time_low::32, time_mid::16, @uuid_v1::4, time_hi::12, rest::binary>>
|> uuid_to_string(format)
end

#
# Internal utility functions.
#
Expand Down Expand Up @@ -584,6 +693,13 @@ defmodule UUID do
<<rnd_hi::7, 1::1, rnd_low::40>>
end

defp uuid6_node(:mac_address) do
uuid1_node()
end
defp uuid6_node(:random_bytes) do
:crypto.strong_rand_bytes(6)
end

# Generate a hash of the given data.
defp namebased_uuid(:md5, data) do
md5 = :crypto.hash(:md5, data)
Expand Down
12 changes: 6 additions & 6 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
%{
"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm", "23f27cbc482cbac03fc8926441eb60a5e111759c17642bac005c3225f5eb809d"},
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm", "5e8806285d8a3a8999bd38e4a73c58d28534c856bc38c44818e5ba85bbda16fb"},
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm", "00e3ebdc821fb3a36957320d49e8f4bfa310d73ea31c90e5f925dc75e030da8f"},
}
2 changes: 1 addition & 1 deletion test/doc_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule UUID.DocTest do
use ExUnit.Case, async: true

doctest UUID, except: [uuid1: 1, uuid1: 3, uuid4: 0, uuid4: 1, uuid4: 2]
doctest UUID, except: [uuid1: 1, uuid1: 3, uuid4: 0, uuid4: 1, uuid4: 2, uuid6: 2, uuid6: 3]
end
3 changes: 3 additions & 0 deletions test/info_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ urn v4 rfc4122 || [uuid: "urn:uuid:184064df-820d-4fd2-9301-4749098cb786", binary
default v5 rfc4122 || [uuid: "dda8df72-e4a1-5b98-a88d-8197e539c0bf", binary: <<221, 168, 223, 114, 228, 161, 91, 152, 168, 141, 129, 151, 229, 57, 192, 191>>, type: :default, version: 5, variant: :rfc4122] || dda8df72-e4a1-5b98-a88d-8197e539c0bf
hex v5 rfc4122 || [uuid: "dda8df72e4a15b98a88d8197e539c0bf", binary: <<221, 168, 223, 114, 228, 161, 91, 152, 168, 141, 129, 151, 229, 57, 192, 191>>, type: :hex, version: 5, variant: :rfc4122] || dda8df72e4a15b98a88d8197e539c0bf
urn v5 rfc4122 || [uuid: "urn:uuid:dda8df72-e4a1-5b98-a88d-8197e539c0bf", binary: <<221, 168, 223, 114, 228, 161, 91, 152, 168, 141, 129, 151, 229, 57, 192, 191>>, type: :urn, version: 5, variant: :rfc4122] || urn:uuid:dda8df72-e4a1-5b98-a88d-8197e539c0bf
default v6 rfc4122 || [uuid: "1e65da3a-36e8-617e-9fcc-c8bcc8a0b17d", binary: <<30, 101, 218, 58, 54, 232, 97, 126, 159, 204, 200, 188, 200, 160, 177, 125>>, type: :default, version: 6, variant: :rfc4122] || 1e65da3a-36e8-617e-9fcc-c8bcc8a0b17d
hex v6 rfc4122 || [uuid: "1e65da3a36e8617e9fccc8bcc8a0b17d", binary: <<30, 101, 218, 58, 54, 232, 97, 126, 159, 204, 200, 188, 200, 160, 177, 125>>, type: :hex, version: 6, variant: :rfc4122] || 1e65da3a36e8617e9fccc8bcc8a0b17d
urn v6 rfc4122 || [uuid: "urn:uuid:1e65da3a-36e8-617e-9fcc-c8bcc8a0b17d", binary: <<30, 101, 218, 58, 54, 232, 97, 126, 159, 204, 200, 188, 200, 160, 177, 125>>, type: :urn, version: 6, variant: :rfc4122] || urn:uuid:1e65da3a-36e8-617e-9fcc-c8bcc8a0b17d
16 changes: 16 additions & 0 deletions test/uuid_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ defmodule UUIDTest do
)
end

test "UUID v1 to UUID v6 conversion" do
uuid1 = UUID.uuid1() |> validate_uuid(1)
assert uuid1 == UUID.uuid1_to_uuid6(uuid1) |> validate_uuid(6) |> UUID.uuid6_to_uuid1()
end

test "UUID v6 to UUID v1 conversion" do
uuid6 = UUID.uuid6() |> validate_uuid(6)
assert uuid6 == UUID.uuid6_to_uuid1(uuid6) |> validate_uuid(1) |> UUID.uuid1_to_uuid6()
end

# Expand the lines in info_tests.txt into individual tests for the
# UUID.info!/1 and UUID.info/1 functions, assuming the lines are:
# test name || expected output || input value
Expand All @@ -39,12 +49,18 @@ defmodule UUIDTest do
{expected, []} = Code.eval_string(unquote(expected))
result = UUID.info!(unquote(input))
assert ^expected = result
validate_uuid(UUID.binary_to_string!(result[:binary]), expected[:version])
end
test "UUID.info/1 #{name}" do
{expected, []} = Code.eval_string(unquote(expected))
{:ok, result} = UUID.info(unquote(input))
assert ^expected = result
validate_uuid(UUID.binary_to_string!(result[:binary]), expected[:version])
end
end

defp validate_uuid(uuid, version) when version in 1..6 do
assert Regex.match?(~r/^[0-9a-f]{8}-[0-9a-f]{4}-#{version}[0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i, uuid)
uuid
end
end