From f06f318e5a23f6725812d1de1db13be4727e038c Mon Sep 17 00:00:00 2001 From: Yaroslav Rogov Date: Thu, 26 Dec 2019 14:11:00 +0300 Subject: [PATCH 1/2] Add type and function specs --- lib/uuid.ex | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++-- mix.exs | 9 +++++--- mix.lock | 2 ++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lib/uuid.ex b/lib/uuid.ex index e651e78..18453ab 100644 --- a/lib/uuid.ex +++ b/lib/uuid.ex @@ -5,6 +5,47 @@ defmodule UUID do See [RFC 4122](http://www.ietf.org/rfc/rfc4122.txt). """ + @typedoc "One of representations of UUID." + @type t :: str | raw | hex | urn + + @typedoc "String representation of UUID." + @type str :: <<_ :: 288>> + + @typedoc "Raw binary representation of UUID." + @type raw :: <<_ :: 128>> + + @typedoc "Hex representation of UUID." + @type hex :: <<_ :: 256>> + + @typedoc "URN representation of UUID." + @type urn :: <<_ :: 360>> + + @typedoc "Type of UUID representation." + @type type :: :default | :raw | :hex | :urn + + @typedoc "UUID version." + @type version :: 1 | 3 | 4 | 5 + + @typedoc "Variant of UUID: see RFC for the details." + @type variant :: :reserved_future + | :reserved_microsoft + | :rfc4122 + | :reserved_ncs + + @typedoc """ + Namespace for UUID v3 and v5 (with some predefined UUIDs as atom aliases). + """ + @type namespace :: :dns | :url | :oid | :x500 | :nil | str + + @typedoc "Information about given UUID (see `info/1`)" + @type info :: [ + uuid: str, + binary: raw, + type: type, + version: version, + variant: variant + ] + @nanosec_intervals_offset 122_192_928_000_000_000 # 15 Oct 1582 to 1 Jan 1970. @nanosec_intervals_factor 10 # Microseconds to nanoseconds factor. @@ -69,6 +110,7 @@ defmodule UUID do ``` """ + @spec info(str) :: {:ok, info} | {:error, any} def info(uuid) do try do {:ok, UUID.info!(uuid)} @@ -126,6 +168,7 @@ defmodule UUID do ``` """ + @spec info!(str) :: info def info!(<> = uuid_string) do {type, <>} = uuid_string_to_hex_pair(uuid) <<_::48, version::4, _::12, v0::1, v1::1, v2::1, _::61>> = <> @@ -167,6 +210,8 @@ defmodule UUID do ``` """ + @spec binary_to_string!(raw) :: str + @spec binary_to_string!(raw, type) :: t def binary_to_string!(uuid, format \\ :default) def binary_to_string!(<>, format) do uuid_to_string(<>, format) @@ -203,6 +248,7 @@ defmodule UUID do ``` """ + @spec string_to_binary!(str) :: raw def string_to_binary!(<>) do {_type, <>} = uuid_string_to_hex_pair(uuid) <> @@ -238,6 +284,8 @@ defmodule UUID do ``` """ + @spec uuid1() :: str + @spec uuid1(type) :: t def uuid1(format \\ :default) do uuid1(uuid1_clockseq(), uuid1_node(), format) end @@ -270,6 +318,8 @@ defmodule UUID do ``` """ + @spec uuid1(clock_seq :: <<_::14>>, node :: <<_::48>>) :: str + @spec uuid1(clock_seq :: <<_::14>>, node :: <<_::48>>, type) :: t def uuid1(clock_seq, node, format \\ :default) def uuid1(<>, <>, format) do <> = uuid1_time() @@ -317,6 +367,8 @@ defmodule UUID do ``` """ + @spec uuid3(namespace, name :: binary) :: str + @spec uuid3(namespace, name :: binary, type) :: t def uuid3(namespace_or_uuid, name, format \\ :default) def uuid3(:dns, <>, format) do namebased_uuid(:md5, <<0x6ba7b8109dad11d180b400c04fd430c8::128, name::binary>>) @@ -375,8 +427,10 @@ defmodule UUID do ``` """ + @spec uuid4() :: str def uuid4(), do: uuid4(:default) + @spec uuid4(type | :strong | :weak) :: t def uuid4(:strong), do: uuid4(:default) # For backwards compatibility. def uuid4(:weak), do: uuid4(:default) # For backwards compatibility. def uuid4(format) do @@ -419,6 +473,8 @@ defmodule UUID do ``` """ + @spec uuid5(namespace, binary) :: str + @spec uuid5(namespace, name :: binary, type) :: t def uuid5(namespace_or_uuid, name, format \\ :default) def uuid5(:dns, <>, format) do namebased_uuid(:sha1, <<0x6ba7b8109dad11d180b400c04fd430c8::128, name::binary>>) @@ -618,7 +674,7 @@ defmodule UUID do defp variant(_) do raise ArgumentError, message: "Invalid argument; Not valid variant bits" end - + defp hex_str_to_binary(<< a1, a2, a3, a4, a5, a6, a7, a8, b1, b2, b3, b4, c1, c2, c3, c4, @@ -633,7 +689,7 @@ defmodule UUID do d(e5)::4, d(e6)::4, d(e7)::4, d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4 >> end - + @compile {:inline, d: 1} defp d(?0), do: 0 diff --git a/mix.exs b/mix.exs index 63339e4..0624572 100644 --- a/mix.exs +++ b/mix.exs @@ -24,9 +24,12 @@ defmodule UUID.Mixfile do # List of dependencies. defp deps do - [{:ex_doc, "~> 0.19", only: :dev}, - {:earmark, "~> 1.2", only: :dev}, - {:benchfella, "~> 0.3", only: :dev}] + [ + {:ex_doc, "~> 0.19", only: :dev}, + {:earmark, "~> 1.2", only: :dev}, + {:benchfella, "~> 0.3", only: :dev}, + {:dialyxir, only: :dev}, + ] end # Description. diff --git a/mix.lock b/mix.lock index abf4863..e1202b4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,8 @@ %{ "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"}, + "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [: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"}, From 28a7ee8829c55376632e7d0ebeb9888828fbde6a Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Mon, 12 Oct 2020 19:59:23 -0700 Subject: [PATCH 2/2] Add UUID v6 Use uuid identifiers module attributes in uuid6/1 Build the UUID v6 instead of converting a v1 Add uuid1<->uuid6 conversion functions Add some more validations to tests --- README.md | 13 +++++ bench/uuid_bench.exs | 9 ++++ lib/uuid.ex | 116 +++++++++++++++++++++++++++++++++++++++++++ mix.lock | 12 ++--- test/doc_test.exs | 2 +- test/info_tests.txt | 3 ++ test/uuid_test.exs | 16 ++++++ 7 files changed, 164 insertions(+), 7 deletions(-) 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