defmodule Uniq.UUID do
@moduledoc """
This module provides RFC 4122 compliant universally unique identifiers (UUIDs).
See the [README](README.md) for general usage information.
"""
import Bitwise, except: ["~~~": 1, &&&: 2, |||: 2, "^^^": 2, <<<: 2, >>>: 2]
import Kernel, except: [to_string: 1]
defstruct [:format, :version, :variant, :time, :seq, :node, :bytes]
@type t :: <<_::128>>
@type formatted ::
t
| <<_::360>>
| <<_::288>>
| <<_::256>>
| <<_::176>>
@type format :: :default | :raw | :hex | :urn | :slug
@type namespace :: :dns | :url | :oid | :x500 | nil | formatted
@type info :: %__MODULE__{
format: :raw | :hex | :default | :urn | :slug,
version: 1..8,
variant: bitstring,
time: non_neg_integer,
seq: non_neg_integer,
node: <<_::48>>,
bytes: t
}
@formats [:default, :raw, :hex, :urn, :slug]
@namespaces [:dns, :url, :oid, :x500, nil]
# Namespaces
@dns_namespace_id Base.decode16!("6ba7b8109dad11d180b400c04fd430c8", case: :lower)
@url_namespace_id Base.decode16!("6ba7b8119dad11d180b400c04fd430c8", case: :lower)
@oid_namespace_id Base.decode16!("6ba7b8129dad11d180b400c04fd430c8", case: :lower)
@x500_namespace_id Base.decode16!("6ba7b8149dad11d180b400c04fd430c8", case: :lower)
@nil_id <<0::128>>
# Variants
@reserved_ncs <<0::1>>
@rfc_variant <<2::2>>
@reserved_ms <<6::3>>
@reserved_future <<7::3>>
defmacrop bits(n), do: quote(do: bitstring - size(unquote(n)))
defmacrop bytes(n), do: quote(do: binary - size(unquote(n)))
defmacrop uint(n), do: quote(do: unsigned - integer - size(unquote(n)))
defmacrop biguint(n), do: quote(do: big - unsigned - integer - size(unquote(n)))
@doc """
Generates a UUID using the version 1 scheme, as described in RFC 4122
This scheme is based on a few key properties:
* A timestamp, based on the count of 100-nanosecond intervals since the start of
the Gregorian calendar, i.e. October 15th, 1582, in Coordinated Universal Time (UTC).
* A clock sequence number, used to ensure that UUIDs generated with the same timestamp
are still unique, by incrementing the sequence each time a UUID is generated with the
same timestamp as the last UUID that was generated. This sequence is initialized with
random bytes at startup, to protect against conflicts.
* A node identifier, which is based on the MAC address of one of the network interfaces
on the system, or if unavailable, using random bytes. In our case, we specifically look
for the first network interface returned by `:inet.getifaddrs/0` that is up, broadcastable,
and has a hardware address, otherwise falling back to cryptographically strong random bytes.
"""
@spec uuid1() :: t
@spec uuid1(format) :: t
def uuid1(format \\ :default) do
{time, clock} = Uniq.Generator.next()
uuid1(time, clock, mac_address(), format)
end
@doc """
This function is the same as `uuid/1`, except the caller provides the clock sequence
value and the node identifier (which must be a 6-byte binary).
See `uuid/1` for details.
"""
@spec uuid1(clock_seq :: non_neg_integer, node :: <<_::48>>, format) :: t
def uuid1(clock_seq, <<node::bits(48)>>, format \\ :default)
when is_integer(clock_seq) and format in @formats do
{time, _} = Uniq.Generator.next()
uuid1(time, clock_seq, node, format)
end
defp uuid1(time, clock_seq, node, format) do
<<thi::12, tmid::16, tlo::32>> = <<time::biguint(60)>>
# Encode version into high bits of timestamp
thi = bor(thi, bsl(1, 12))
# Encode variant into high bits of clock sequence
clock_hi = bsr(band(clock_seq, 0x3F00), 8)
clock_hi = bor(clock_hi, 0x80)
clock_lo = band(clock_seq, 0xFF)
raw = <<tlo::32, tmid::16, thi::16, clock_hi::8, clock_lo::8, node::bits(48)>>
format(raw, format)
end
@doc """
Generates a UUID using the version 3 scheme, as described in RFC 4122
This scheme provides the means for generating UUIDs deterministically,
given a namespace and a name. This means that with the same inputs, you
get the same UUID as output.
The main difference between this and the version 5 scheme, is that version 3
uses MD5 for hashing, and version 5 uses SHA1. Both hashes are deprecated these
days, but you should prefer version 5 unless otherwise required.
In this scheme, the timestamp, clock sequence and node value are constructed
from the namespace and name, as described in RFC 4122, Section 4.3.
## Namespaces
You may choose one of several options for namespacing your UUIDs:
1. Use a predefined namespace. These are provided by RFC 4122 in order to provide
namespacing for common types of names. See below.
2. Use your own namespace. For this, simply generate a UUID to represent the namespace.
You may provide this UUID in whatever format is supported by `parse/1`.
3. Use `nil`. This is bound to a special-case UUID that has no intrinsic meaning, but is
valid for use as a namespace.
The set of predefined namespaces consist of the following:
* `:dns`, intended for namespacing fully-qualified domain names
* `:url`, intended for namespacing URLs
* `:oid`, intended for namespacing ISO OIDs
* `:x500`, intended for namespacing X.500 DNs (in DER or text output format)
## Notes
One thing to be aware of with version 3 and 5 UUIDs, is that unlike version 1 and 6,
the lexicographical ordering of UUIDs of generated one after the other, is entirely
random, as the most significant bits are dependent upon the hash of the namespace and
name, and thus not based on time or even the lexicographical ordering of the name.
This is generally worth the tradeoff in favor of determinism, but it is something to
be aware of.
Likewise, since the generation is deterministic, care must be taken to ensure that you
do not try to use the same name for two different objects within the same namespace. This
should be obvious, but since the other schemes are _not_ sensitive in this way, it is worth
calling out.
"""
@spec uuid3(namespace, name :: binary) :: t
@spec uuid3(namespace, name :: binary, format) :: t
def uuid3(namespace, name, format \\ :default)
when (namespace in @namespaces or is_binary(namespace)) and is_binary(name) and
format in @formats do
namespaced_uuid(3, :md5, namespace, name, format)
end
@doc """
Generates a UUID using the version 4 scheme, as described in RFC 4122
This scheme is like the version 1 scheme, except it uses randomly generated data
for the timestamp, clock sequence, and node fields.
This scheme is the closest you can get to truly unique identifiers, as they are based
on truly random (or pseudo-random) data, so the chances of generating the same UUID
twice is astronomically small.
## Notes
The version 4 scheme does have some deficiencies. Namely, since they are based on random
data, the lexicographical ordering of the resulting UUID is itself random, which can play havoc
with database indices should you choose to use UUIDs for primary keys.
It is strongly recommended to consider the version 6 scheme instead. They are almost the
same as a version 1 UUID, but with improved semantics that combine some of the beneficial
traits of version 4 UUIDs without the lexicographical ordering downsides. The only caveat
to that recommendation is if you need to pass them through a system that inspects the UUID
encoding itself and doesn't have preliminary support for version 6.
"""
@spec uuid4() :: t
@spec uuid4(format) :: t
def uuid4(format \\ :default) when format in @formats do
<<tlo_mid::48, _::4, thi::12, _::2, rest::62>> = :crypto.strong_rand_bytes(16)
raw = <<tlo_mid::48, 4::biguint(4), thi::12, @rfc_variant, rest::62>>
format(raw, format)
end
@doc """
Generates a UUID using the version 5 scheme, as described in RFC 4122
This scheme provides the means for generating UUIDs deterministically,
given a namespace and a name. This means that with the same inputs, you
get the same UUID as output.
The main difference between this and the version 5 scheme, is that version 3
uses MD5 for hashing, and version 5 uses SHA1. Both hashes are deprecated these
days, but you should prefer version 5 unless otherwise required.
In this scheme, the timestamp, clock sequence and node value are constructed
from the namespace and name, as described in RFC 4122, Section 4.3.
## Namespaces
You may choose one of several options for namespacing your UUIDs:
1. Use a predefined namespace. These are provided by RFC 4122 in order to provide
namespacing for common types of names. See below.
2. Use your own namespace. For this, simply generate a UUID to represent the namespace.
You may provide this UUID in whatever format is supported by `parse/1`.
3. Use `nil`. This is bound to a special-case UUID that has no intrinsic meaning, but is
valid for use as a namespace.
The set of predefined namespaces consist of the following:
* `:dns`, intended for namespacing fully-qualified domain names
* `:url`, intended for namespacing URLs
* `:oid`, intended for namespacing ISO OIDs
* `:x500`, intended for namespacing X.500 DNs (in DER or text output format)
## Notes
One thing to be aware of with version 3 and 5 UUIDs, is that unlike version 1 and 6,
the lexicographical ordering of UUIDs of generated one after the other, is entirely
random, as the most significant bits are dependent upon the hash of the namespace and
name, and thus not based on time or even the lexicographical ordering of the name.
This is generally worth the tradeoff in favor of determinism, but it is something to
be aware of.
Likewise, since the generation is deterministic, care must be taken to ensure that you
do not try to use the same name for two different objects within the same namespace. This
should be obvious, but since the other schemes are _not_ sensitive in this way, it is worth
calling out.
"""
@spec uuid5(namespace, name :: binary) :: t
@spec uuid5(namespace, name :: binary, format) :: t
def uuid5(namespace, name, format \\ :default)
when (namespace in @namespaces or is_binary(namespace)) and is_binary(name) and
format in @formats do
namespaced_uuid(5, :sha, namespace, name, format)
end
@doc """
Generates a UUID using the proposed version 6 scheme, found
[here](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#section-5.1).
This is a draft extension of RFC 4122, but has not yet been formally accepted.
Version 6 provides the following benefits over versions 1 and 4:
* Like version 1, it is time-based, but unlike version 1, it is naturally sortable by time
in its raw binary encoded form
* Like version 4, it provides better guarantees of uniqueness and privacy, by basing itself
on random or pseudo-random data, rather than MAC addresses and other potentially sensitive
information.
* Unlike version 4, which tends to interact poorly with database indices due to being derived
entirely from random or pseudo-random data; version 6 ensures that the most significant bits
of the binary encoded form are a 1:1 match with the most significant bits of the timestamp on
which it was derived. This guarantees that version 6 UUIDs are naturally sortable in the order
in which they were generated (with some randomness among those which are generated at the same
time).
There have been a number of similar proposals that address the same set of flaws. For example:
* [KSUID](https://github.com/segmentio/ksuid)
* [ULID](https://github.com/ulid/spec)
Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead.
"""
@spec uuid6() :: t
@spec uuid6(format) :: t
def uuid6(format \\ :default) when format in @formats do
{time, clock} = Uniq.Generator.next()
node = :crypto.strong_rand_bytes(6)
# Deconstruct timestamp
<<thi::48, tlo::12>> = <<time::biguint(60)>>
# Encode the version to the most significant bits of the last octet of the timestamp
tlo_and_version = <<6::4, tlo::12>>
# Encode the variant in the most significant bits of the clock sequence
clock_seq = <<@rfc_variant, clock::biguint(14)>>
raw = <<thi::48, tlo_and_version::bits(16), clock_seq::bits(16), node::bits(48)>>
format(raw, format)
end
@doc """
Generates a UUID using the proposed version 7 scheme, found
[here](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#section-5.2).
This is a draft extension of RFC 4122, but has not yet been formally accepted.
UUID version 7 features a time-ordered value field derived from the widely implemented and well
known Unix Epoch timestamp source, the number of milliseconds seconds since midnight 1 Jan 1970
UTC, leap seconds excluded. As well as improved entropy characteristics over versions 1 or 6.
Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible.
"""
@spec uuid7() :: t
@spec uuid7(format) :: t
def uuid7(format \\ :default) when format in @formats do
time = System.system_time(:millisecond)
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
raw = <<time::biguint(48), 7::4, rand_a::12, @rfc_variant, rand_b::62>>
format(raw, format)
end
defp namespaced_uuid(version, algorithm, namespace, name, format) do
id = namespace_id(namespace)
<<tlo_mid::48, _::4, thi::12, _::2, rest::62>> = hash(algorithm, id <> name)
raw = <<tlo_mid::48, version::4, thi::12, @rfc_variant, rest::62>>
format(raw, format)
end
@doc """
Like `info/1`, but raises if the input UUID is invalid.
"""
@spec info!(binary, :struct) :: info | no_return
@spec info!(binary, :keyword) :: Keyword.t() | no_return
def info!(bin, style \\ :struct)
def info!(bin, style) when is_binary(bin) do
with {:ok, info} <- info(bin, style) do
info
else
{:error, reason} ->
raise ArgumentError, message: "invalid uuid: #{inspect(reason)}"
end
end
def info!(_, _) do
raise ArgumentError, message: "invalid uuid: :invalid_format"
end
@doc """
This function parses the given UUID, in any of the supported encodings/formats, and produces
the information gleaned from the encoded data.
Two styles of information are supported, depending on whether the function is called via
the compatibility shim for `:elixir_uuid`, or directly. You may pass `:struct` or `:keyword`
manually if you wish to express a preference for one style or the other.
The `:struct` form is the UUID structure used internally by this library, and it contains all
of the information needed to re-encode the UUID as binary.
The `:keyword` form matches 1:1 the keyword list produced by `UUID.info/1` provided by the
`:elixir_uuid` library, and it contains slightly less information, but is useful for compatibility
with legacy code that operates on that structure.
# Examples
iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf", :keyword)
{:ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf",
binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>,
type: :default,
version: 4,
variant: :rfc4122]}
iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf")
{:ok, %Uniq.UUID{
format: :default,
version: 4,
variant: <<2::2>>,
time: 326283406408022248,
seq: 790,
node: <<129, 224, 137, 184, 194, 207>>,
bytes: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>,
}}
"""
@spec info(binary, :struct) :: {:ok, info} | {:error, term}
@spec info(binary, :keyword) :: {:ok, Keyword.t()} | {:error, term}
def info(bin, style \\ :struct)
# Compatibility with :elixir_uuid's info
def info(bin, :keyword) when is_binary(bin) do
with {:ok, uuid} <- parse(bin) do
{:ok,
[
uuid: uuid |> to_string() |> String.downcase(),
binary: uuid.bytes,
type: uuid.format,
version: uuid.version,
variant: format_variant(uuid.variant)
]}
end
end
def info(bin, :struct) when is_binary(bin) do
parse(bin)
end
def info(_, style) when style in [:keyword, :struct],
do: {:error, :invalid_format}
@doc """
Returns true if the given string is a valid UUID.
## Options
* `strict: boolean`, if true, requires strict RFC 4122 conformance,
i.e. version 6 is considered invalid
"""
@spec valid?(binary) :: boolean
@spec valid?(binary, Keyword.t()) :: boolean
def valid?(bin, opts \\ [])
def valid?(bin, opts) do
strict? = Keyword.get(opts, :strict, false)
case parse(bin) do
{:ok, %__MODULE__{version: 6}} when strict? ->
false
{:ok, _} ->
true
{:error, _} ->
false
end
end
@doc """
Parses a `#{__MODULE__}` from a binary.
Supported formats include human-readable strings, as well as
the raw binary form of the UUID.
## Examples
iex> {:ok, uuid} = Uniq.UUID.parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
{:ok, %Uniq.UUID{
bytes: <<248, 29, 79, 174, 125, 236, 17, 208, 167, 101, 0, 160, 201, 30, 107, 246>>,
format: :default,
node: <<0, 160, 201, 30, 107, 246>>,
seq: 10085,
time: 130742845922168750,
variant: <<2::size(2)>>,
version: 1
}}
...> {:ok, %Uniq.UUID{uuid | format: :urn}} == Uniq.UUID.parse("urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
true
iex> match?({:ok, %Uniq.UUID{format: :default, version: 1}}, Uniq.UUID.uuid1() |> Uniq.UUID.parse())
true
"""
@spec parse(binary) :: {:ok, info} | {:error, term}
def parse(bin)
def parse("urn:uuid:" <> uuid) do
with {:ok, uuid} <- parse(uuid) do
{:ok, %__MODULE__{uuid | format: :urn}}
end
end
def parse(<<_::128>> = bin),
do: parse_raw(bin, %__MODULE__{format: :raw})
def parse(<<bin::bytes(32)>>) do
bin
|> decode_hex()
|> parse_raw(%__MODULE__{format: :hex})
rescue
ArgumentError ->
{:error, {:invalid_format, :hex}}
end
def parse(<<a::bytes(8), ?-, b::bytes(4), ?-, c::bytes(4), ?-, d::bytes(4), ?-, e::bytes(12)>>) do
with {:ok, bin} <- Base.decode16(a <> b <> c <> d <> e, case: :mixed) do
parse_raw(bin, %__MODULE__{format: :default})
else
:error ->
{:error, {:invalid_format, :default}}
end
end
def parse(<<uuid::bytes(22)>>) do
with {:ok, value} <- Base.url_decode64(uuid <> "==") do
parse_raw(value, %__MODULE__{format: :slug})
else
_ ->
{:error, {:invalid_format, :slug}}
end
end
def parse(_bin), do: {:error, :invalid_format}
# Parse version
defp parse_raw(<<_::48, version::uint(4), _::bitstring>> = bin, acc) do
case version do
v when v in [1, 3, 4, 5, 6, 7] ->
with {:ok, uuid} <- parse_raw(version, bin, acc) do
{:ok, %__MODULE__{uuid | bytes: bin}}
end
_ when bin == @nil_id ->
{:ok, %__MODULE__{acc | bytes: @nil_id}}
_ ->
{:error, {:unknown_version, version}}
end
end
# Parse variant
defp parse_raw(version, <<time::64, @reserved_ncs, rest::bits(63)>>, acc),
do: parse_raw(version, @reserved_ncs, time, rest, acc)
defp parse_raw(version, <<time::64, @rfc_variant, rest::bits(62)>>, acc),
do: parse_raw(version, @rfc_variant, time, rest, acc)
defp parse_raw(version, <<time::64, @reserved_ms, rest::bits(61)>>, acc),
do: parse_raw(version, @reserved_ms, time, rest, acc)
defp parse_raw(version, <<time::64, @reserved_future, rest::bits(61)>>, acc),
do: parse_raw(version, @reserved_future, time, rest, acc)
defp parse_raw(_version, <<_time::64, variant::bits(3), _rest::bits(61)>>, _acc) do
{:error, {:unknown_variant, variant}}
end
for variant <- [@reserved_ncs, @rfc_variant, @reserved_ms, @reserved_future] do
variant_size = bit_size(variant)
variant = Macro.escape(variant)
# Parses RFC 4122, version 1-5 uuids
defp parse_raw(version, unquote(variant), time, rest, acc) when version < 6 do
variant_size = unquote(variant_size)
clock_hi_size = 8 - variant_size
clock_size = 8 + clock_hi_size
with <<time_lo::bits(32), time_mid::bits(16), _version::4, time_hi::bits(12)>> <-
<<time::64>>,
<<timestamp::uint(60)>> <-
<<time_hi::bits(12), time_mid::bits(16), time_lo::bits(32)>>,
<<clock_hi::bits(clock_hi_size), clock_lo::bits(8), node::bits(48)>> <-
rest,
<<clock::uint(clock_size)>> <-
<<clock_hi::bits(clock_hi_size), clock_lo::bits(8)>> do
{:ok,
%__MODULE__{
acc
| version: version,
variant: unquote(variant),
time: timestamp,
seq: clock,
node: node
}}
else
other ->
{:error, {:invalid_format, other, variant_size, clock_hi_size, clock_size}}
end
end
end
# Parses proposed version 7 uuids
defp parse_raw(7, <<1::1, 0::1>> = variant, time, rest, acc) do
with <<time::biguint(48), _version::4, _rand_a::12>> <- <<time::64>>,
<<_rand_b::62>> <- rest do
{:ok,
%__MODULE__{
acc
| version: 7,
variant: variant,
time: time
}}
else
_ ->
{:error, {:invalid_format, :v7}}
end
end
# Parses proposed version 6 uuids, which are very much like version 1, but with some field ordering changes
defp parse_raw(6, <<1::1, 0::1>> = variant, time, rest, acc) do
with <<time_hi::48, _version::4, time_lo::12>> <- <<time::64>>,
<<timestamp::uint(60)>> <- <<time_hi::48, time_lo::12>>,
<<clock::uint(14), node::bits(48)>> <-
rest do
{:ok,
%__MODULE__{
acc
| version: 6,
variant: variant,
time: timestamp,
seq: clock,
node: node
}}
else
_ ->
{:error, {:invalid_format, :v6}}
end
end
defp parse_raw(6, variant, _time, _rest, _acc), do: {:error, {:invalid_variant, variant}}
# Handles proposed version 7 and 8 uuids
defp parse_raw(version, _variant, _time, _rest, _acc),
do: {:error, {:unsupported_version, version}}
@doc """
Formats a `#{__MODULE__}` as a string, using the format it was originally generated with.
See `to_string/2` if you want to specify what format to produce.
"""
@spec to_string(formatted | info) :: String.t()
def to_string(uuid)
def to_string(<<raw::bits(128)>>),
do: format(raw, :default)
def to_string(uuid) when is_binary(uuid) do
uuid
|> string_to_binary!()
|> format(:default)
end
def to_string(%__MODULE__{bytes: raw, format: format}),
do: format(raw, format)
@doc """
Same as `to_string/1`, except you can specify the desired format.
The `format` can be one of the following:
* `:default`, produces strings like `"f81d4fae-7dec-11d0-a765-00a0c91e6bf6"`
* `:urn`, produces strings like `"urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"`
* `:hex`, produces strings like `"f81d4fae7dec11d0a76500a0c91e6bf6"`
* `:slug`, produces strings like `"-B1Prn3sEdCnZQCgyR5r9g=="`
* `:raw`, produces the raw binary encoding of the uuid in 128 bits
"""
@spec to_string(formatted | info, format) :: String.t()
def to_string(uuid, format)
def to_string(<<raw::bits(128)>>, format) when format in @formats,
do: format(raw, format)
def to_string(uuid, format) when is_binary(uuid) and format in @formats do
uuid
|> string_to_binary!()
|> format(format)
end
def to_string(%__MODULE__{bytes: raw}, format) when format in @formats,
do: format(raw, format)
@doc """
This function takes a UUID string in any of the formats supported by `to_string/1`,
and returns the raw, binary-encoded form.
"""
@spec string_to_binary!(String.t()) :: t | no_return
def string_to_binary!(str)
def string_to_binary!(<<_::128>> = uuid), do: uuid
def string_to_binary!(<<hex::bytes(32)>>) do
decode_hex(hex)
end
def string_to_binary!(
<<a::bytes(8), ?-, b::bytes(4), ?-, c::bytes(4), ?-, d::bytes(4), ?-, e::bytes(12)>>
) do
decode_hex(a <> b <> c <> d <> e)
end
def string_to_binary!(<<slug::bytes(22)>>) do
with {:ok, value} <- Base.url_decode64(slug <> "==") do
value
else
_ ->
raise ArgumentError, message: "invalid uuid string"
end
end
def string_to_binary!(_) do
raise ArgumentError, message: "invalid uuid string"
end
@doc """
Compares two UUIDs, using their canonical 128-bit integer form, as described in RFC 4122.
You may provide the UUIDs in either string, binary, or as a `Uniq.UUID` struct.
"""
@spec compare(String.t() | info, String.t() | info) :: :lt | :eq | :gt
def compare(a, b)
def compare(%__MODULE__{} = a, %__MODULE__{} = b) do
a = to_canonical_integer(a)
b = to_canonical_integer(b)
do_compare(a, b)
end
def compare(%__MODULE__{} = a, <<b::biguint(128)>>) do
a = to_canonical_integer(a)
do_compare(a, b)
end
def compare(%__MODULE__{} = a, b) when is_binary(b) do
a = to_canonical_integer(a)
b = string_to_binary!(b)
do_compare(a, b)
end
def compare(<<a::biguint(128)>>, %__MODULE__{} = b) do
b = to_canonical_integer(b)
do_compare(a, b)
end
def compare(a, %__MODULE__{} = b) when is_binary(a) do
a = string_to_binary!(a)
b = to_canonical_integer(b)
do_compare(a, b)
end
def compare(<<a::biguint(128)>>, <<b::biguint(128)>>),
do: do_compare(a, b)
def compare(a, b) when is_binary(a) and is_binary(b) do
a = to_string(a)
b = to_string(b)
do_compare(a, b)
end
defp do_compare(a, b) do
cond do
a < b ->
:lt
a == b ->
:eq
:else ->
:gt
end
end
defp to_canonical_integer(%__MODULE__{bytes: <<value::biguint(128)>>}) do
value
end
@doc false
def format(raw, format)
def format(raw, :raw), do: raw
def format(raw, :default), do: format_default(raw)
def format(raw, :hex), do: encode_hex(raw)
def format(raw, :urn), do: "urn:uuid:#{format(raw, :default)}"
def format(raw, :slug), do: Base.url_encode64(raw, padding: false)
@compile {:inline, [format_default: 1]}
defp format_default(<<
a1::4,
a2::4,
a3::4,
a4::4,
a5::4,
a6::4,
a7::4,
a8::4,
b1::4,
b2::4,
b3::4,
b4::4,
c1::4,
c2::4,
c3::4,
c4::4,
d1::4,
d2::4,
d3::4,
d4::4,
e1::4,
e2::4,
e3::4,
e4::4,
e5::4,
e6::4,
e7::4,
e8::4,
e9::4,
e10::4,
e11::4,
e12::4
>>) do
<<e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-, e(b1), e(b2), e(b3), e(b4), ?-,
e(c1), e(c2), e(c3), e(c4), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),
e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
end
@doc false
defp format_variant(@reserved_future), do: :reserved_future
defp format_variant(@reserved_ms), do: :reserved_microsoft
defp format_variant(@rfc_variant), do: :rfc4122
defp format_variant(@reserved_ncs), do: :reserved_ncs
defp format_variant(_), do: :unknown
defp mac_address do
candidate_interface? = fn {_if, info} ->
flags = Keyword.get(info, :flags, [])
Enum.member?(flags, :up) and Enum.member?(flags, :broadcast) and
Keyword.has_key?(info, :hwaddr)
end
with {:ok, interfaces} <- :inet.getifaddrs(),
{_if, info} <- Enum.find(interfaces, candidate_interface?) do
IO.iodata_to_binary(info[:hwaddr])
else
_ ->
# In lieu of a MAC address, we can generate an equivalent number of random bytes
<<head::7, _::1, tail::46>> = :crypto.strong_rand_bytes(6)
# Ensure the multicast bit is set, as per RFC 4122
<<head::7, 1::1, tail::46>>
end
end
defp hash(:md5, data), do: :crypto.hash(:md5, data)
defp hash(:sha, data), do: :binary.part(:crypto.hash(:sha, data), 0, 16)
defp namespace_id(:dns), do: @dns_namespace_id
defp namespace_id(:url), do: @url_namespace_id
defp namespace_id(:oid), do: @oid_namespace_id
defp namespace_id(:x500), do: @x500_namespace_id
defp namespace_id(nil), do: @nil_id
defp namespace_id(<<_::128>> = ns), do: ns
defp namespace_id(<<ns::bytes(32)>>) do
with {:ok, raw} <- Base.decode16(ns, case: :mixed) do
raw
else
_ ->
invalid_namespace!()
end
end
defp namespace_id(
<<a::bytes(8), ?-, b::bytes(4), ?-, c::bytes(4), ?-, d::bytes(4), ?-, e::bytes(12)>>
) do
with {:ok, raw} <- Base.decode16(a <> b <> c <> d <> e, case: :mixed) do
raw
else
_ ->
invalid_namespace!()
end
end
defp namespace_id(<<ns::bytes(22)>>) do
with {:ok, raw} <- Base.url_decode64(ns <> "==") do
raw
else
_ ->
invalid_namespace!()
end
end
defp namespace_id(_ns), do: invalid_namespace!()
defp invalid_namespace!,
do:
raise(ArgumentError,
message: "expected a valid namespace atom (:dns, :url, :oid, :x500), or a UUID string"
)
import Uniq.Macros, only: [defextension: 2, defshim: 3]
@compile {:inline, [encode_hex: 1, decode_hex: 1]}
defshim encode_hex(bin), to: :binary do
defp encode_hex(bin), do: IO.iodata_to_binary(for <<bs::4 <- bin>>, do: e(bs))
end
defshim decode_hex(bin), to: :binary do
defp decode_hex(
<<a1, a2, a3, a4, a5, a6, a7, a8, b1, b2, b3, b4, c1, c2, c3, c4, d1, d2, d3, d4, e1,
e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
) do
<<d(a1)::4, d(a2)::4, d(a3)::4, d(a4)::4, d(a5)::4, d(a6)::4, d(a7)::4, d(a8)::4, d(b1)::4,
d(b2)::4, d(b3)::4, d(b4)::4, d(c1)::4, d(c2)::4, d(c3)::4, d(c4)::4, d(d1)::4, d(d2)::4,
d(d3)::4, d(d4)::4, d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4, d(e5)::4, d(e6)::4, d(e7)::4,
d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4>>
catch
:throw, char ->
raise ArgumentError, message: "#{inspect(<<char::utf8>>)} is not valid hex"
end
@compile {:inline, d: 1}
defp d(?0), do: 0
defp d(?1), do: 1
defp d(?2), do: 2
defp d(?3), do: 3
defp d(?4), do: 4
defp d(?5), do: 5
defp d(?6), do: 6
defp d(?7), do: 7
defp d(?8), do: 8
defp d(?9), do: 9
defp d(?A), do: 10
defp d(?B), do: 11
defp d(?C), do: 12
defp d(?D), do: 13
defp d(?E), do: 14
defp d(?F), do: 15
defp d(?a), do: 10
defp d(?b), do: 11
defp d(?c), do: 12
defp d(?d), do: 13
defp d(?e), do: 14
defp d(?f), do: 15
defp d(char), do: throw(char)
end
@compile {:inline, e: 1}
defp e(0), do: ?0
defp e(1), do: ?1
defp e(2), do: ?2
defp e(3), do: ?3
defp e(4), do: ?4
defp e(5), do: ?5
defp e(6), do: ?6
defp e(7), do: ?7
defp e(8), do: ?8
defp e(9), do: ?9
defp e(10), do: ?a
defp e(11), do: ?b
defp e(12), do: ?c
defp e(13), do: ?d
defp e(14), do: ?e
defp e(15), do: ?f
## Ecto
defextension Ecto.ParameterizedType do
use Ecto.ParameterizedType
@doc false
@impl Ecto.ParameterizedType
def init(opts) do
schema = Keyword.fetch!(opts, :schema)
field = Keyword.fetch!(opts, :field)
format = Keyword.get(opts, :format, :default)
dump = Keyword.get(opts, :dump, :raw)
type = Keyword.get(opts, :type)
unless format in @formats do
raise ArgumentError,
message:
"invalid :format option, expected one of #{Enum.join(@formats, ",")}; got #{inspect(format)}"
end
unless dump in @formats do
raise ArgumentError,
message:
"invalid :dump option, expected one of #{Enum.join(@formats, ",")}; got #{inspect(format)}"
end
version = Keyword.get(opts, :version, 4)
unless version in [1, 3, 4, 5, 6, 7] do
raise ArgumentError,
message:
"invalid uuid version, expected one of 1, 3, 4, 5, 6, or 7; got #{inspect(version)}"
end
namespace = Keyword.get(opts, :namespace)
case namespace do
nil when version in [3, 5] ->
raise ArgumentError,
message: "you must set :namespace to a valid uuid when :version is 3 or 5"
nil ->
:ok
ns when ns in [:dns, :url, :oid, :x500] ->
raise ArgumentError,
message:
"you must set :namespace to a uuid, the predefined namespaces are not permitted here"
ns when is_binary(ns) ->
:ok
ns ->
raise ArgumentError,
message: "expected :namespace to be a binary, but got #{inspect(ns)}"
end
%{
schema: schema,
field: field,
format: format,
dump: dump,
type: type,
version: version,
namespace: namespace
}
end
@doc false
@impl Ecto.ParameterizedType
def type(%{type: nil, dump: :raw}), do: :binary
def type(%{type: nil}), do: :string
def type(%{type: t}), do: t
# This is provided as a helper for autogenerating version 3 or 5 uuids
@doc false
@impl Ecto.ParameterizedType
def autogenerate(%{format: format, version: version, namespace: namespace}) do
case version do
1 ->
uuid1(format)
4 ->
uuid4(format)
6 ->
uuid6(format)
7 ->
uuid7(format)
v when v in [3, 5] ->
# 64 bits of entropy should be more than sufficient, since the total entropy
# of the input here is 192 bits, which we get from the namespace (128 bits) + the name (64 bits).
# That is then represented using only 128 bits (an entire MD5 hash, or 128 of the
# 160 bits of a SHA1 hash). In short, its doubtful that using more than 8 bytes
# of random data is going to have any appreciable benefit on uniqueness. Discounting
# the namespace, the total entropy is only 64 bits, which in practice is constrained
# by the hash itself, which is then further constrained by the fact that 6 bits of the
# UUID are reserved for version and variant information. In short, even though we are
# assuming a namespace that can contain 2^64 unique values, in practice it is less than
# that, though it still leaves room for an astronomical number of unique identifiers.
name = :crypto.strong_rand_bytes(8)
case v do
3 -> uuid3(namespace, name, format)
5 -> uuid5(namespace, name, format)
end
end
end
@doc false
@impl Ecto.ParameterizedType
def cast(data, params)
def cast(uuid, %{format: format}) when is_binary(uuid) do
{:ok, to_string(uuid, format)}
rescue
ArgumentError ->
:error
end
def cast(%__MODULE__{} = uuid, %{format: format}),
do: {:ok, to_string(uuid, format)}
def cast(nil, _params), do: {:ok, nil}
def cast(_, _params), do: :error
@doc false
@impl Ecto.ParameterizedType
def load(value, loader, params)
def load(uuid, _loader, %{format: format}) when is_binary(uuid) do
{:ok, to_string(uuid, format)}
rescue
ArgumentError ->
:error
end
def load(nil, _loader, _params),
do: {:ok, nil}
@doc false
@impl Ecto.ParameterizedType
def dump(value, dumper, params)
def dump(%__MODULE__{} = uuid, _dumper, %{dump: format}),
do: {:ok, to_string(uuid, format)}
def dump(uuid, _dumper, %{dump: format}) when is_binary(uuid) do
{:ok, to_string(uuid, format)}
rescue
ArgumentError ->
:error
end
def dump(nil, _dumper, _params),
do: {:ok, nil}
@doc false
@impl Ecto.ParameterizedType
def embed_as(_format, _params), do: :self
@doc false
@impl Ecto.ParameterizedType
def equal?(a, b, params)
def equal?(nil, nil, _), do: true
def equal?(nil, b, _), do: to_string(b, :raw) == @nil_id
def equal?(a, nil, _), do: to_string(a, :raw) == @nil_id
def equal?(a, b, _), do: compare(to_string(a), to_string(b)) == :eq
end
defimpl String.Chars do
alias Uniq.UUID
def to_string(uuid), do: UUID.to_string(uuid)
end
defimpl Inspect do
import Inspect.Algebra
def inspect(%Uniq.UUID{bytes: bytes, version: version}, opts) do
# Allow overriding the format in which UUIDs are displayed via custom inspect options
format = Keyword.get(opts.custom_options, :format, :default)
uuid = Uniq.UUID.to_string(bytes, format)
concat(["#UUIDv", Kernel.to_string(version), "<", uuid, ">"])
end
end
end