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

Commit

Permalink
Add UUID v6
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ryanwinchester committed Oct 14, 2020
1 parent 118ebdc commit 4adc190
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 7 deletions.
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

0 comments on commit 4adc190

Please sign in to comment.