%%% Copyright 2022 Nomasystems, S.L. http://www.nomasystems.com %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%% -module(nuid). -moduledoc """ Unique identifier generation. This module produces two families of identifiers as `t:t/0` binaries: - **UUIDs.** Versions 1, 3, 4, and 5 per [RFC 4122](https://www.rfc-editor.org/rfc/rfc4122), and versions 6, 7, and 8 per [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562), plus the nil and max UUIDs. Rendered as the canonical 36-character hyphenated form, e.g. `<<"018b3d7a-9f9a-7577-adb2-08761e3d87f7">>`. - **nuids.** `nuid1/0` and `nuid2/0`, two Nomasystems identifiers that are lexicographically sortable by creation time and carry 128 bits of cryptographically strong randomness. They are encoded with a URL-safe, sortable base64 variant (see `m:nuid_base64`). Time-based and random identifiers (`uuid1/0`, `uuid4/0`, `uuid6/0`, `uuid7/0`, `nuid1/0`, `nuid2/0`) are non-deterministic. The name-based identifiers (`uuid3/2`, `uuid5/2`) are deterministic: the same `t:namespace/0` and name always produce the same UUID. The `*_info/1` functions recover the creation time (and, where encoded, the originating node) from an identifier. ## Examples ```erlang 1> nuid:uuid4(). <<"37a9e737-f680-44a9-b83d-a517ec758b75">> 2> nuid:uuid5(dns, <<"nomasystems.com">>). <<"cefe05b2-95ca-5b0a-ad06-9b3f2b38e532">> 3> nuid:uuid7(). <<"018b3d7a-9f9a-7577-adb2-08761e3d87f7">> 4> nuid:nuid2(). <<"OHtpP-----Fkn3F6JaT5Kxnm_NAiDzFgGMzc">> ``` """. %%% INCLUDE FILES -include_lib("nuid/include/nuid.hrl"). %%% EXTERNAL EXPORTS %RFC 4122 -export([uuid1/0, uuid3/2, uuid4/0, uuid5/2]). % RFC 9562 (formerly draft-ietf-uuidrev-rfc4122bis) -export([uuid6/0, uuid7/0, uuid8/1, uuid8/3]). -export([uuid1_info/1, uuid6_info/1, uuid7_info/1]). -export([nil_uuid/0, max_uuid/0]). -export([nuid1/0, nuid1_info/1]). -export([nuid2/0, nuid2_info/1]). %%% TYPES -export_type([t/0, namespace/0, uuid_info/0, datetime/0]). -doc "A generated identifier as a printable binary.". -type t() :: binary(). -doc """ Namespace for name-based UUIDs. The atoms select the predefined RFC 4122 namespace UUIDs; a binary is used verbatim as a custom namespace. """. -type namespace() :: dns | url | oid | x500 | nil | binary(). -doc """ Decoded creation time, embedded counter, and originating node of a time-based UUID. Defined by the `uuidInfo` record in `nuid.hrl`. """. -type uuid_info() :: #uuidInfo{}. %%% MACROS -define(JANUARY_1ST_1970, 62167219200). -define(INTEGER_38_BIT_WRAP, 274877906944). -define(BIT24_SPACE, 16777216). -define(RAND_BYTES, 16). -define(V1, 1). % -define(V2, 2). %%There are no v2 implementations -define(V3, 3). -define(V4, 4). -define(V5, 5). -define(V6, 6). -define(V7, 7). -define(V8, 8). -define(PRECISION, 16). -define(VARIANT, 2). -define(HEX(X), X + $0 + (39 * (X div 10))). -define(INT(X), ((X - $0) - ((X div $A) * 7) - ((X div $a) * 32))). %%%----------------------------------------------------------------------------- %%% EXTERNAL EXPORTS %%%----------------------------------------------------------------------------- -doc "Generate a time-based RFC 4122 version 1 UUID.". -spec uuid1() -> t(). uuid1() -> Timestamp = erlang:system_time(micro_seconds), Unique = erlang:unique_integer([positive, monotonic]), % Ensure increasing GUIDs on 38 bit wrap Time = Timestamp + (Unique div ?INTEGER_38_BIT_WRAP), <> = <>, <> = <>, NodeId = erlang:phash2(node()), format_uuid( <>, 0, [] ). -doc "Recover creation time, counter, and node from a version 1 UUID.". -spec uuid1_info(t()) -> uuid_info(). uuid1_info(Bin) -> uuid_info(Bin, uuid1). -doc """ Generate a name-based RFC 4122 version 3 UUID (MD5). Deterministic: the same `t:namespace/0` and name always yield the same UUID. """. -spec uuid3(namespace(), binary()) -> t(). uuid3(dns, Name) when is_binary(Name) -> compose_uuid(md5, ?V3, <<16#6ba7b8109dad11d180b400c04fd430c8:128, Name/binary>>); uuid3(url, Name) when is_binary(Name) -> compose_uuid(md5, ?V3, <<16#6ba7b8119dad11d180b400c04fd430c8:128, Name/binary>>); uuid3(oid, Name) when is_binary(Name) -> compose_uuid(md5, ?V3, <<16#6ba7b8129dad11d180b400c04fd430c8:128, Name/binary>>); uuid3(x500, Name) when is_binary(Name) -> compose_uuid(md5, ?V3, <<16#6ba7b8149dad11d180b400c04fd430c8:128, Name/binary>>); uuid3(nil, Name) when is_binary(Name) -> compose_uuid(md5, ?V3, <<0:128, Name/binary>>); uuid3(NameSpace, Name) when is_binary(NameSpace), is_binary(Name) -> compose_uuid(md5, ?V3, <>). -doc "Generate a random RFC 4122 version 4 UUID (122 random bits).". -spec uuid4() -> t(). uuid4() -> <> = crypto:strong_rand_bytes(16), format_uuid(<>, 0, []). -doc """ Generate a name-based RFC 4122 version 5 UUID (SHA-1). Deterministic: the same `t:namespace/0` and name always yield the same UUID. """. -spec uuid5(namespace(), binary()) -> t(). uuid5(dns, Name) when is_binary(Name) -> compose_uuid(sha, ?V5, <<16#6ba7b8109dad11d180b400c04fd430c8:128, Name/binary>>); uuid5(url, Name) when is_binary(Name) -> compose_uuid(sha, ?V5, <<16#6ba7b8119dad11d180b400c04fd430c8:128, Name/binary>>); uuid5(oid, Name) when is_binary(Name) -> compose_uuid(sha, ?V5, <<16#6ba7b8129dad11d180b400c04fd430c8:128, Name/binary>>); uuid5(x500, Name) when is_binary(Name) -> compose_uuid(sha, ?V5, <<16#6ba7b8149dad11d180b400c04fd430c8:128, Name/binary>>); uuid5(nil, Name) when is_binary(Name) -> compose_uuid(sha, ?V5, <<0:128, Name/binary>>); uuid5(NameSpace, Name) when is_binary(NameSpace), is_binary(Name) -> compose_uuid(sha, ?V5, <>). -doc """ Generate a time-ordered RFC 9562 version 6 UUID. The most significant bits hold the timestamp, so version 6 UUIDs sort lexicographically by creation time. """. -spec uuid6() -> t(). uuid6() -> Timestamp = erlang:system_time(micro_seconds), Unique = erlang:unique_integer([positive, monotonic]), % Ensure increasing GUIDs on 38 bit wrap Time = Timestamp + (Unique div ?INTEGER_38_BIT_WRAP), <> = <>, <> = <>, NodeId = erlang:phash2(node()), format_uuid( <>, 0, [] ). -doc "Recover creation time, counter, and node from a version 6 UUID.". -spec uuid6_info(t()) -> uuid_info(). uuid6_info(Bin) -> uuid_info(Bin, uuid6). -doc """ Generate a time-ordered RFC 9562 version 7 UUID. A 48-bit Unix millisecond timestamp followed by 74 random bits. Sorts lexicographically by creation time. """. -spec uuid7() -> t(). uuid7() -> Timestamp = erlang:system_time(milli_seconds), <> = crypto:strong_rand_bytes(10), format_uuid(<>, 0, []). -doc "Recover the creation time from a version 7 UUID.". -spec uuid7_info(t()) -> datetime(). uuid7_info(Bin) -> uuid_info(Bin, uuid7). -doc """ Generate a vendor-specific RFC 9562 version 8 UUID from a 128-bit binary. The version and variant bits in the input are overwritten; all other bits are preserved. """. -spec uuid8(binary()) -> t(). uuid8(<>) -> uuid8(CustomA, CustomB, CustomC). -doc """ Generate a vendor-specific RFC 9562 version 8 UUID from its three custom fields (48, 12, and 62 bits). """. -spec uuid8(non_neg_integer(), non_neg_integer(), non_neg_integer()) -> t(). uuid8(CustomA, CustomB, CustomC) -> format_uuid(<>, 0, []). %%%----------------------------------------------------------------------------- %%% EXTERNAL NIL and ZERO FUNCTIONS %%%----------------------------------------------------------------------------- -doc "Return the RFC 4122 nil UUID (all zero bits).". -spec nil_uuid() -> t(). nil_uuid() -> <<"00000000-0000-0000-0000-000000000000">>. -doc "Return the RFC 9562 max UUID (all one bits).". -spec max_uuid() -> t(). max_uuid() -> <<"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF">>. %%%----------------------------------------------------------------------------- %%% EXTERNAL PROPOSED FUNCTIONS %%%----------------------------------------------------------------------------- -doc """ Generate a `nuid1` identifier. A hex microsecond timestamp, a separator, and 16 cryptographically strong random bytes in sortable base64. Lexicographically sortable and greater than any previously generated version 6 UUID. """. -spec nuid1() -> t(). nuid1() -> Timestamp = erlang:system_time(micro_seconds), Unique = erlang:unique_integer([positive, monotonic]), % Ensure increasing IDs on 38 bit wrap Time = Timestamp + (Unique div ?INTEGER_38_BIT_WRAP), Rand = crypto:strong_rand_bytes(?RAND_BYTES), TimeBin = to_hex(Time), RandBase64 = nuid_base64:encode(Rand), <>. -doc "Recover the creation time from a `nuid1` identifier.". -spec nuid1_info(t()) -> datetime(). nuid1_info(<>) -> RawTime = erlang:binary_to_integer(HexTime, 16), case RawTime of Time when Time > 0 -> calendar:gregorian_seconds_to_datetime((Time div 1000000) + ?JANUARY_1ST_1970); _Error -> erlang:throw({error, badarg}) end. -doc """ Generate a `nuid2` identifier. A POSIX-second timestamp, a sortable counter, 3 bytes of node origin, and 16 cryptographically strong random bytes, all in sortable base64. Lexicographically sortable, URL-safe, and no longer than a UUID. """. -spec nuid2() -> t(). nuid2() -> Timestamp = erlang:system_time(seconds), Unique = erlang:unique_integer([positive, monotonic]), Node = erlang:phash2(node(), ?BIT24_SPACE), Rand = crypto:strong_rand_bytes(?RAND_BYTES), nuid_base64:encode(<>). -doc """ Recover the originating node and creation time from a `nuid2` identifier. The node is resolved against the currently connected nodes; it is `undefined` if the originating node is not reachable. """. -spec nuid2_info(t()) -> {node() | undefined, datetime()}. nuid2_info(Id) when is_binary(Id) -> <> = nuid_base64:decode(Id), Node = proplists:get_value( NodeId, [{erlang:phash2(X, ?BIT24_SPACE), X} || X <- [node() | nodes()]] ), {Node, calendar:gregorian_seconds_to_datetime(Time + ?JANUARY_1ST_1970)}. %%%----------------------------------------------------------------------------- %%% INTERNAL FUNCTIONS %%%----------------------------------------------------------------------------- compose_uuid(Cipher, Version, Binary) -> Hash = crypto:hash(Cipher, Binary), <> = Hash, format_uuid(<>, 0, []). format_uuid(_Bin, ?PRECISION, Acc) -> list_to_binary(lists:reverse(Acc)); format_uuid(Bin, Pos, Acc) when Pos == 3; Pos == 5; Pos == 7; Pos == 9 -> Byte = binary:at(Bin, Pos), High = Byte div 16, Low = Byte rem 16, format_uuid(Bin, Pos + 1, [$-, ?HEX(Low), ?HEX(High) | Acc]); format_uuid(Bin, Pos, Acc) -> Byte = binary:at(Bin, Pos), High = Byte div 16, Low = Byte rem 16, format_uuid(Bin, Pos + 1, [?HEX(Low), ?HEX(High) | Acc]). to_hex(Id) -> to_hex(Id, <<>>). to_hex(0, <<>>) -> <<"0">>; to_hex(0, Acc) -> Acc; to_hex(Id, Acc) -> Mod = Id rem 16, Div = Id div 16, to_hex(Div, <<(?HEX(Mod)), Acc/binary>>). uuid_info(<>, Type) -> uuid_info(<>, 0, <<>>, Type). uuid_info(_Bin, 32, Info, uuid1) -> get_uuid1_info(Info); uuid_info(_Bin, 32, Info, uuid6) -> get_uuid6_info(Info); uuid_info(_Bin, 32, Info, uuid7) -> get_uuid7_info(Info); uuid_info(Bin, Pos, Acc, Type) -> H1 = binary:at(Bin, Pos), H2 = binary:at(Bin, Pos + 1), Byte = (?INT(H1) * 16) + ?INT(H2), uuid_info(Bin, Pos + 2, <>, Type). get_uuid1_info( <> ) -> <> = <>, <> = <>, get_uuid16_info(Time, Unique, NodeId). get_uuid6_info( <> ) -> <> = <>, <> = <>, get_uuid16_info(Time, Unique, NodeId). get_uuid16_info(Time, Unique, NodeId) -> {{Year, Month, Day}, {Hour, Min, Sec}} = calendar:gregorian_seconds_to_datetime( (Time div 1000000) + ?JANUARY_1ST_1970 ), NodeIdF = fun(Node) -> Id = erlang:phash2(Node), <> = <>, Id28 end, #uuidInfo{ date = {{Year, Month, Day}, {Hour, Min, Sec}}, id = Unique, node = proplists:get_value(NodeId, [{NodeIdF(X), X} || X <- [node() | nodes()]]) }. get_uuid7_info(<>) -> calendar:gregorian_seconds_to_datetime((Time div 1000) + ?JANUARY_1ST_1970).