diff --git a/README.md b/README.md index ff344e3..512845e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bench/uuid_bench.exs b/bench/uuid_bench.exs index 0ce674b..d2c1ff7 100644 --- a/bench/uuid_bench.exs +++ b/bench/uuid_bench.exs @@ -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 diff --git a/lib/uuid.ex b/lib/uuid.ex index e651e78..37c3375 100644 --- a/lib/uuid.ex +++ b/lib/uuid.ex @@ -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. @@ -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(<>, <>, format) do + <> = uuid1_time() + <> = <> + <> = <> + + <> + |> uuid_to_string(format) + end + def uuid6(_, _, _) do + raise ArgumentError, message: + "Invalid argument; Expected: <>, <>" + 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) + + <> = ub1 + <> = <> + + <> + |> 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) + + <> = ub6 + <> = <> + + <> + |> uuid_to_string(format) + end + # # Internal utility functions. # @@ -584,6 +693,13 @@ defmodule UUID do <> 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) diff --git a/mix.lock b/mix.lock index abf4863..ac8d467 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, } diff --git a/test/doc_test.exs b/test/doc_test.exs index 48c9bb5..668271f 100644 --- a/test/doc_test.exs +++ b/test/doc_test.exs @@ -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 diff --git a/test/info_tests.txt b/test/info_tests.txt index d0d2ede..6b7d06c 100644 --- a/test/info_tests.txt +++ b/test/info_tests.txt @@ -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 diff --git a/test/uuid_test.exs b/test/uuid_test.exs index b3ec395..9bcc72f 100644 --- a/test/uuid_test.exs +++ b/test/uuid_test.exs @@ -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 @@ -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