Skip to content

Commit af154d6

Browse files
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
1 parent 346581c commit af154d6

6 files changed

Lines changed: 155 additions & 7 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ iex> UUID.uuid5("fcfe5f21-8a08-4c9a-9f97-29d2fd6a27b9", "my.domain.com")
6262
"b8e85535-761a-586f-9c04-0fb0df2cbe84"
6363
```
6464

65+
### UUID v6
66+
67+
Generated using a combination of time since the west adopted the gregorian calendar and either the node id MAC address or random bytes.
68+
Valid node types are `:mac_address` or `:random_bytes` and defaults to `:mac_address`.
69+
70+
```elixir
71+
iex> UUID.uuid6()
72+
"1eb0d1d0-126a-6495-9a93-171634969e27"
73+
74+
iex> UUID.uuid6(:random_bytes)
75+
"1eb0d1d5-c3fa-6b2e-8d7a-ef182baf6b94"
76+
```
77+
6578
### Formatting
6679

6780
All UUID generator functions have an optional format parameter as the last argument.

lib/uuid.ex

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule UUID do
1313
@uuid_v3 3 # UUID v3 identifier.
1414
@uuid_v4 4 # UUID v4 identifier.
1515
@uuid_v5 5 # UUID v5 identifier.
16+
@uuid_v6 6 # UUID v6 identifier.
1617

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

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

454+
@doc """
455+
Generate a new UUID v6. This version uses a combination of one or more of:
456+
unix epoch, random bytes, pid hash, and hardware address.
457+
458+
Accepts a `node_type` argument that can be either `:mac_address` or
459+
`:random_bytes`. Defaults to `:mac_address`. However, if there is a security
460+
concern with using a MAC address, use `:random_bytes`.
461+
462+
See the [RFC draft, section 3.3](https://tools.ietf.org/html/draft-peabody-dispatch-new-uuid-format-00#section-3.3)
463+
for more information on the node parts.
464+
465+
## Examples
466+
467+
iex> UUID.uuid6()
468+
"1eb0d28f-da4c-6eb2-adc1-0242ac120002"
469+
470+
iex> UUID.uuid6(:random_bytes, :default)
471+
"1eb0d297-eb1e-62a6-a37f-a55eda5dd6e4"
472+
473+
iex> UUID.uuid6(:random_bytes, :hex)
474+
"1eb0d298502563fcadcd25e5d0a44c1a"
475+
476+
iex> UUID.uuid6(:random_bytes, :urn)
477+
"urn:uuid:1eb0d298-ca10-6914-ab0e-7d7e1e6e1808"
478+
479+
iex> UUID.uuid6(:random_bytes, :raw)
480+
<<30, 176, 210, 153, 52, 23, 102, 230, 164, 146, 99, 66, 4, 72, 220, 114>>
481+
482+
iex> UUID.uuid6(:random_bytes, :slug)
483+
"HrDSmab8ZnqR4SKw4LN-UA"
484+
485+
"""
486+
def uuid6(node_type \\ :mac_address, format \\ :default)
487+
when node_type in [:mac_address, :random_bytes] do
488+
uuid6(uuid1_clockseq(), uuid6_node(node_type), format)
489+
end
490+
491+
@doc """
492+
Generate a new UUID v6, with an existing clock sequence and node address. This
493+
version uses a combination of one or more of: unix epoch, random bytes,
494+
pid hash, and hardware address.
495+
"""
496+
def uuid6(<<clock_seq::14>>, <<node::48>>, format) do
497+
<<time_hi::12, time_mid::16, time_low::32>> = uuid1_time()
498+
<<time_low1::20, time_low2::12>> = <<time_low::32>>
499+
<<clock_seq_hi::6, clock_seq_low::8>> = <<clock_seq::14>>
500+
501+
<<time_hi::12, time_mid::16, time_low1::20, @uuid_v6::4, time_low2::12,
502+
@variant10::2, clock_seq_hi::6, clock_seq_low::8, node::48>>
503+
|> uuid_to_string(format)
504+
end
505+
def uuid6(_, _, _) do
506+
raise ArgumentError, message:
507+
"Invalid argument; Expected: <<clock_seq::14>>, <<node::48>>"
508+
end
509+
510+
@doc """
511+
Convert a UUID v1 to a UUID v6 in the same format.
512+
513+
## Examples
514+
515+
iex> UUID.uuid1_to_uuid6("dafc431a-0d21-11eb-adc1-0242ac120002")
516+
"1eb0d21d-afc4-631a-adc1-0242ac120002"
517+
518+
iex> UUID.uuid1_to_uuid6("2vxDGg0hEeutwQJCrBIAAg")
519+
"HrDSHa_EYxqtwQJCrBIAAg"
520+
521+
iex> UUID.uuid1_to_uuid6(<<218, 252, 67, 26, 13, 33, 17, 235, 173, 193, 2, 66, 172, 18, 0, 2>>)
522+
<<30, 176, 210, 29, 175, 196, 99, 26, 173, 193, 2, 66, 172, 18, 0, 2>>
523+
524+
"""
525+
def uuid1_to_uuid6(uuid1) do
526+
{format, ub1} = uuid_string_to_hex_pair(uuid1)
527+
528+
<<time_low::32, time_mid::16, @uuid_v1::4, time_hi::12, rest::binary>> = ub1
529+
<<time_low1::20, time_low2::12>> = <<time_low::32>>
530+
531+
<<time_hi::12, time_mid::16, time_low1::20, @uuid_v6::4, time_low2::12,
532+
rest::binary>>
533+
|> uuid_to_string(format)
534+
end
535+
536+
@doc """
537+
Convert a UUID v6 to a UUID v1 in the same format.
538+
539+
## Examples
540+
541+
iex> UUID.uuid6_to_uuid1("1eb0d21d-afc4-631a-adc1-0242ac120002")
542+
"dafc431a-0d21-11eb-adc1-0242ac120002"
543+
544+
iex> UUID.uuid6_to_uuid1("HrDSHa_EYxqtwQJCrBIAAg")
545+
"2vxDGg0hEeutwQJCrBIAAg"
546+
547+
iex> UUID.uuid6_to_uuid1(<<30, 176, 210, 29, 175, 196, 99, 26, 173, 193, 2, 66, 172, 18, 0, 2>>)
548+
<<218, 252, 67, 26, 13, 33, 17, 235, 173, 193, 2, 66, 172, 18, 0, 2>>
549+
550+
"""
551+
def uuid6_to_uuid1(uuid6) do
552+
{format, ub6} = uuid_string_to_hex_pair(uuid6)
553+
554+
<<time_hi::12, time_mid::16, time_low1::20, @uuid_v6::4, time_low2::12,
555+
rest::binary>> = ub6
556+
<<time_low::32>> = <<time_low1::20, time_low2::12>>
557+
558+
<<time_low::32, time_mid::16, @uuid_v1::4, time_hi::12, rest::binary>>
559+
|> uuid_to_string(format)
560+
end
561+
453562
#
454563
# Internal utility functions.
455564
#
@@ -584,6 +693,13 @@ defmodule UUID do
584693
<<rnd_hi::7, 1::1, rnd_low::40>>
585694
end
586695

696+
defp uuid6_node(:mac_address) do
697+
uuid1_node()
698+
end
699+
defp uuid6_node(:random_bytes) do
700+
:crypto.strong_rand_bytes(6)
701+
end
702+
587703
# Generate a hash of the given data.
588704
defp namebased_uuid(:md5, data) do
589705
md5 = :crypto.hash(:md5, data)

mix.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
%{
2-
"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"},
3-
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
4-
"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"},
5-
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
6-
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
7-
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
2+
"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm", "23f27cbc482cbac03fc8926441eb60a5e111759c17642bac005c3225f5eb809d"},
3+
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm", "5e8806285d8a3a8999bd38e4a73c58d28534c856bc38c44818e5ba85bbda16fb"},
4+
"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"},
5+
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
6+
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
7+
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm", "00e3ebdc821fb3a36957320d49e8f4bfa310d73ea31c90e5f925dc75e030da8f"},
88
}

test/doc_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule UUID.DocTest do
22
use ExUnit.Case, async: true
33

4-
doctest UUID, except: [uuid1: 1, uuid1: 3, uuid4: 0, uuid4: 1, uuid4: 2]
4+
doctest UUID, except: [uuid1: 1, uuid1: 3, uuid4: 0, uuid4: 1, uuid4: 2, uuid6: 2, uuid6: 3]
55
end

test/info_tests.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ urn v4 rfc4122 || [uuid: "urn:uuid:184064df-820d-4fd2-9301-4749098cb786", binary
1010
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
1111
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
1212
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
13+
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
14+
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
15+
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

test/uuid_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ defmodule UUIDTest do
2929
)
3030
end
3131

32+
test "UUID v1 to UUID v6 conversion" do
33+
uuid1 = UUID.uuid1() |> validate_uuid(1)
34+
assert uuid1 == UUID.uuid1_to_uuid6(uuid1) |> validate_uuid(6) |> UUID.uuid6_to_uuid1()
35+
end
36+
37+
test "UUID v6 to UUID v1 conversion" do
38+
uuid6 = UUID.uuid6() |> validate_uuid(6)
39+
assert uuid6 == UUID.uuid6_to_uuid1(uuid6) |> validate_uuid(1) |> UUID.uuid1_to_uuid6()
40+
end
41+
3242
# Expand the lines in info_tests.txt into individual tests for the
3343
# UUID.info!/1 and UUID.info/1 functions, assuming the lines are:
3444
# test name || expected output || input value
@@ -39,12 +49,18 @@ defmodule UUIDTest do
3949
{expected, []} = Code.eval_string(unquote(expected))
4050
result = UUID.info!(unquote(input))
4151
assert ^expected = result
52+
validate_uuid(UUID.binary_to_string!(result[:binary]), expected[:version])
4253
end
4354
test "UUID.info/1 #{name}" do
4455
{expected, []} = Code.eval_string(unquote(expected))
4556
{:ok, result} = UUID.info(unquote(input))
4657
assert ^expected = result
58+
validate_uuid(UUID.binary_to_string!(result[:binary]), expected[:version])
4759
end
4860
end
4961

62+
defp validate_uuid(uuid, version) when version in 1..6 do
63+
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)
64+
uuid
65+
end
5066
end

0 commit comments

Comments
 (0)