%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: %%% %%%------------------------------------------------------------------------ %%% @doc %%% ==Erlang UUID Generation== %%% [http://www.ietf.org/rfc/rfc4122.txt] is the reference for official UUIDs. %%% This implementation provides a version 1 UUID that includes both the %%% Erlang pid identifier (ID, Serial, Creation) and the distributed Erlang %%% node name within the 48 bit node ID. To make room for the Erlang pid %%% identifier, the 48 bits from the MAC address %%% (i.e., 3 OCI (Organizationally Unique Identifier) bytes and %%% 3 NIC (Network Interface Controller) specific bytes) and %%% the distributed Erlang node name are bitwise-XORed down to 16 bits. %%% The Erlang pid is bitwise-XORed from 72 bits down to 32 bits. %%% The version 3 (MD5), version 4 (random), and version 5 (SHA) %%% methods are provided as specified within the RFC. %%% %%% The ordered version 1 variant is not present in the RFC, %%% though it is the most standards-compliant way of providing %%% timestamp UUID ordering. Timestamp ordering has been used in many %%% non-standard UUID formats based on version 1 and typically limited to %%% the same data present in the version 1 UUID %%% (e.g., "Version 6" at http://gh.peabody.io/uuidv6/). %%% @end %%% %%% MIT License %%% %%% Copyright (c) 2011-2023 Michael Truog %%% %%% Permission is hereby granted, free of charge, to any person obtaining a %%% copy of this software and associated documentation files (the "Software"), %%% to deal in the Software without restriction, including without limitation %%% the rights to use, copy, modify, merge, publish, distribute, sublicense, %%% and/or sell copies of the Software, and to permit persons to whom the %%% Software is furnished to do so, subject to the following conditions: %%% %%% The above copyright notice and this permission notice shall be included in %%% all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %%% DEALINGS IN THE SOFTWARE. %%% %%% @author Michael Truog %%% @copyright 2011-2023 Michael Truog %%% @version 2.0.6 {@date} {@time} %%%------------------------------------------------------------------------ -module(uuid). -author('mjtruog at protonmail dot com'). %% external interface -export([new/1, new/2, get_v1/1, get_v1_time/0, get_v1_time/1, get_v1_time_ns/0, get_v1_time_ns/1, get_v1_datetime/1, get_v1_datetime/2, is_v1/1, get_v3/1, get_v3/2, get_v3_compat/1, get_v3_compat/2, is_v3/1, get_v4/0, get_v4/1, get_v4_urandom/0, is_v4/1, get_v5/1, get_v5/2, get_v5_compat/1, get_v5_compat/2, is_v5/1, uuid_to_list/1, uuid_to_string/1, uuid_to_string/2, string_to_uuid/1, is_uuid/1, increment/1, mac_address/0, test/0]). -ifdef(ERLANG_OTP_VERSION_16). -else. -ifdef(ERLANG_OTP_VERSION_17). -else. -define(ERLANG_OTP_VERSION_18_FEATURES, true). -ifdef(ERLANG_OTP_VERSION_18). -else. -define(ERLANG_OTP_VERSION_19_FEATURES, true). -ifdef(ERLANG_OTP_VERSION_19). -else. -define(ERLANG_OTP_VERSION_20_FEATURES, true). -ifdef(ERLANG_OTP_VERSION_20). -else. -ifdef(OTP_RELEASE). % Erlang/OTP >= 21.0 % able to use -elif here -else. -error("Erlang/OTP version invalid"). -endif. -endif. -endif. -endif. -endif. -endif. -ifdef(ERLANG_OTP_VERSION_18_FEATURES). -type timestamp_type_internal() :: 'erlang_timestamp' | 'os' | 'warp'. -else. -type timestamp_type_internal() :: 'erlang_now' | 'os'. -endif. -type v1_variant() :: 'rfc4122' | 'ordered'. -record(uuid_state, { variant :: v1_variant(), node_id :: <<_:48>>, clock_seq :: 0..16383, timestamp_type :: timestamp_type_internal(), timestamp_last :: integer() % nanoseconds div 100 }). -type uuid() :: <<_:128>>. -type timestamp_type() :: 'erlang' | 'os' | 'warp'. -type state() :: #uuid_state{}. -export_type([uuid/0, timestamp_type/0, state/0]). -ifdef(ERLANG_OTP_VERSION_19_FEATURES). -type iso8601() :: nonempty_list($0..$9 | $T | $- | $: | $. | $Z). -type uuid_string_list() :: nonempty_list($0..$9 | $a..$f | $-). -else. -type iso8601() :: nonempty_string(). -type uuid_string_list() :: nonempty_string(). -endif. -type uuid_string_binary() :: <<_:256>> | <<_:288>>. -type uuid_string() :: uuid_string_list() | uuid_string_binary(). -export_type([iso8601/0, uuid_string_list/0, uuid_string_binary/0, uuid_string/0]). -include("uuid.hrl"). % Erlang External Term Format constants % info from http://erlang.org/doc/apps/erts/erl_ext_dist.html -define(TAG_VERSION, 131). -define(TAG_PID_EXT, 103). -define(TAG_NEW_PID_EXT, 88). % 16#01b21dd213814000 is the number of 100-ns intervals between the % UUID epoch 1582-10-15 00:00:00 and the UNIX epoch 1970-01-01 00:00:00. -define(GREGORIAN_EPOCH_OFFSET, 16#01b21dd213814000). %%%------------------------------------------------------------------------ %%% External interface functions %%%------------------------------------------------------------------------ %%------------------------------------------------------------------------- %% @doc %% ===Create new UUID state for v1 UUID generation.=== %% @end %%------------------------------------------------------------------------- -spec new(Pid :: pid()) -> state(). new(Pid) when is_pid(Pid) -> new(Pid, [{timestamp_type, erlang}]). %%------------------------------------------------------------------------- %% @doc %% ===Create new UUID state for v1 UUID generation using a specific type of timestamp.=== %% The timestamp can either be based on erlang's adjustment of time %% (for only strictly monotonically increasing time values) or the %% operating system's time without any adjustment %% (with timestamp_type values `erlang' and `os', respectively). %% If you want erlang's adjustment of time without enforcement of increasing %% time values, use the `warp' timestamp_type value with Erlang >= 18.0. %% @end %%------------------------------------------------------------------------- -spec new(Pid :: pid(), Options :: timestamp_type() | list({timestamp_type, timestamp_type()} | {mac_address, list(non_neg_integer())} | {variant, v1_variant()})) -> state(). new(Pid, TimestampType) when is_pid(Pid), ((TimestampType =:= erlang) orelse (TimestampType =:= os) orelse (TimestampType =:= warp)) -> new(Pid, [{timestamp_type, TimestampType}]); new(Pid, Options) when is_pid(Pid), is_list(Options) -> TimestampType = case lists:keyfind(timestamp_type, 1, Options) of {timestamp_type, Value1} -> Value1; false -> erlang end, MacAddress = case lists:keyfind(mac_address, 1, Options) of {mac_address, Value2} -> Value2; false -> mac_address() end, Variant = case lists:keyfind(variant, 1, Options) of {variant, Value3} -> Value3; false -> rfc4122 end, % make the version 1 UUID specific to the Erlang node and pid % 48 bits for the first MAC address found is included with the % distributed Erlang node name to be hashed with the node creation count NodeData = [MacAddress, erlang:atom_to_binary(node(), utf8)], % Reduce the node information to 16 bits and the pid information to 32 bits % for use as the UUID v1 Node Id {NodeCreationCount, PidData} = case erlang:term_to_binary(Pid) of % (when the pid format changes, handle the different formats) <> -> % 72 bits for the Erlang pid <> = binary:part(PidBin, erlang:byte_size(PidBin), -9), {PidCR, [PidID1, PidID2, PidID3, PidID4, PidSR1, PidSR2, PidSR3, PidSR4]}; % format supported in Erlang/OTP 19.0-rc1 % required for Erlang/OTP 23.0 (and Erlang/OTP 22.0-rc2) <> -> % 96 bits for the Erlang pid <> = binary:part(PidBin, erlang:byte_size(PidBin), -12), {PidCR, [PidID1, PidID2, PidID3, PidID4, PidSR1, PidSR2, PidSR3, PidSR4]} end, Node32 = quickrand_hash:jenkins_32(NodeData, NodeCreationCount), Node16 = (Node32 bsr 16) bxor (Node32 band 16#FFFF), Pid32 = quickrand_hash:jenkins_32(PidData), NodeId = <>, ClockSeq = if Variant =:= rfc4122 -> pseudo_random(16384) - 1; Variant =:= ordered -> pseudo_random(8192) - 1 end, TimestampTypeInternal = if TimestampType =:= os -> os; TimestampType =:= erlang -> timestamp_type_erlang(); TimestampType =:= warp -> case erlang:function_exported(erlang, system_time, 0) of true -> % Erlang >= 18.0 warp; false -> % Erlang < 18.0 erlang:exit(badarg) end end, TimestampLast = timestamp(TimestampTypeInternal), #uuid_state{variant = Variant, node_id = NodeId, clock_seq = ClockSeq, timestamp_type = TimestampTypeInternal, timestamp_last = TimestampLast}. %%------------------------------------------------------------------------- %% @doc %% ===Get a v1 UUID.=== %% @end %%------------------------------------------------------------------------- -spec get_v1(State :: state()) -> {uuid(), StateNew :: state()}. get_v1(#uuid_state{variant = rfc4122, node_id = NodeId, clock_seq = ClockSeq, timestamp_type = TimestampTypeInternal, timestamp_last = TimestampLast} = State) -> Timestamp = timestamp(TimestampTypeInternal, TimestampLast), Time = Timestamp + ?GREGORIAN_EPOCH_OFFSET, % will be larger than 60 bits after 5236-03-31 21:21:00 <> = <>, {<>, State#uuid_state{timestamp_last = Timestamp}}; get_v1(#uuid_state{variant = ordered, node_id = NodeId, clock_seq = ClockSeq, timestamp_type = TimestampTypeInternal, timestamp_last = TimestampLast} = State) -> Timestamp = timestamp(TimestampTypeInternal, TimestampLast), Time = Timestamp + ?GREGORIAN_EPOCH_OFFSET, % will be larger than 60 bits after 5236-03-31 21:21:00 <> = <>, {<>, State#uuid_state{timestamp_last = Timestamp}}. %%------------------------------------------------------------------------- %% @doc %% ===Get the current time value in a manner consistent with the v1 UUID.=== %% The result is an integer in microseconds since the UNIX epoch. %% (The UNIX epoch is 1970-01-01T00:00:00Z) %% @end %%------------------------------------------------------------------------- -spec get_v1_time() -> non_neg_integer(). get_v1_time() -> get_v1_time(erlang). %%------------------------------------------------------------------------- %% @doc %% ===Get the current time value in a manner consistent with the v1 UUID.=== %% The result is an integer in microseconds since the UNIX epoch. %% (The UNIX epoch is 1970-01-01T00:00:00Z) %% @end %%------------------------------------------------------------------------- -spec get_v1_time(timestamp_type() | state() | uuid()) -> non_neg_integer(). get_v1_time(Value) -> get_v1_time_ns(Value) div 1000. %%------------------------------------------------------------------------- %% @doc %% ===Get the current time value in a manner consistent with the v1 UUID.=== %% The result is an integer in nanoseconds since the UNIX epoch. %% (The UNIX epoch is 1970-01-01T00:00:00Z) %% @end %%------------------------------------------------------------------------- -spec get_v1_time_ns() -> non_neg_integer(). get_v1_time_ns() -> get_v1_time_ns(erlang). %%------------------------------------------------------------------------- %% @doc %% ===Get the current time value in a manner consistent with the v1 UUID.=== %% The result is an integer in nanoseconds since the UNIX epoch. %% (The UNIX epoch is 1970-01-01T00:00:00Z) %% @end %%------------------------------------------------------------------------- -spec get_v1_time_ns(timestamp_type() | state() | uuid()) -> non_neg_integer(). -ifdef(ERLANG_OTP_VERSION_18_FEATURES). get_v1_time_ns(erlang) -> timestamp_nanoseconds(erlang_timestamp); get_v1_time_ns(os) -> timestamp_nanoseconds(os); get_v1_time_ns(warp) -> timestamp_nanoseconds(warp); get_v1_time_ns(#uuid_state{timestamp_type = TimestampTypeInternal}) -> timestamp_nanoseconds(TimestampTypeInternal); get_v1_time_ns(Value) when is_binary(Value), byte_size(Value) == 16 -> <> = case Value of <> -> <>; <> -> <> end, (Time - ?GREGORIAN_EPOCH_OFFSET) * 100. -else. get_v1_time_ns(erlang) -> timestamp_nanoseconds(erlang_now); get_v1_time_ns(os) -> timestamp_nanoseconds(os); get_v1_time_ns(warp) -> erlang:exit(badarg); get_v1_time_ns(#uuid_state{timestamp_type = TimestampTypeInternal}) -> timestamp_nanoseconds(TimestampTypeInternal); get_v1_time_ns(Value) when is_binary(Value), byte_size(Value) == 16 -> <> = case Value of <> -> <>; <> -> <> end, (Time - ?GREGORIAN_EPOCH_OFFSET) * 100. -endif. %%------------------------------------------------------------------------- %% @doc %% ===Get an ISO8601 datetime in UTC from a v1 UUID's time value.=== %% http://www.w3.org/TR/NOTE-datetime %% @end %%------------------------------------------------------------------------- -spec get_v1_datetime(Value :: timestamp_type() | state() | uuid() | erlang:timestamp()) -> iso8601(). get_v1_datetime({_, _, MicroSeconds} = Timestamp) -> {{DateYYYY, DateMM, DateDD}, {TimeHH, TimeMM, TimeSS}} = calendar:now_to_universal_time(Timestamp), [DateYYYY0, DateYYYY1, DateYYYY2, DateYYYY3] = int_to_dec_list(DateYYYY, 4), [DateMM0, DateMM1] = int_to_dec_list(DateMM, 2), [DateDD0, DateDD1] = int_to_dec_list(DateDD, 2), [TimeHH0, TimeHH1] = int_to_dec_list(TimeHH, 2), [TimeMM0, TimeMM1] = int_to_dec_list(TimeMM, 2), [TimeSS0, TimeSS1] = int_to_dec_list(TimeSS, 2), [MicroSeconds0, MicroSeconds1, MicroSeconds2, MicroSeconds3, MicroSeconds4, MicroSeconds5] = int_to_dec_list(MicroSeconds, 6), [DateYYYY0, DateYYYY1, DateYYYY2, DateYYYY3, $-, DateMM0, DateMM1, $-, DateDD0, DateDD1, $T, TimeHH0, TimeHH1, $:, TimeMM0, TimeMM1, $:, TimeSS0, TimeSS1, $., MicroSeconds0, MicroSeconds1, MicroSeconds2, MicroSeconds3, MicroSeconds4, MicroSeconds5, $Z]; get_v1_datetime(Value) -> get_v1_datetime(Value, 0). %%------------------------------------------------------------------------- %% @doc %% ===Get an ISO8601 datetime in UTC from a v1 UUID's time value with an offset in microseconds.=== %% http://www.w3.org/TR/NOTE-datetime %% @end %%------------------------------------------------------------------------- -spec get_v1_datetime(Value :: timestamp_type() | state() | uuid() | erlang:timestamp(), MicroSecondsOffset :: integer()) -> iso8601(). get_v1_datetime(Value, MicroSecondsOffset) when is_integer(MicroSecondsOffset) -> MicroSecondsTotal = get_v1_time(Value) + MicroSecondsOffset, MegaSeconds = MicroSecondsTotal div 1000000000000, Seconds = (MicroSecondsTotal div 1000000) - MegaSeconds * 1000000, MicroSeconds = MicroSecondsTotal rem 1000000, get_v1_datetime({MegaSeconds, Seconds, MicroSeconds}). %%------------------------------------------------------------------------- %% @doc %% ===Is the binary a v1 UUID?=== %% @end %%------------------------------------------------------------------------- -spec is_v1(Value :: any()) -> boolean(). is_v1(<<_TimeLow:32, _TimeMid:16, 0:1, 0:1, 0:1, 1:1, % version 1 bits _TimeHigh:12, 1:1, 0:1, % RFC 4122 variant bits _ClockSeq:14, _NodeId:48>>) -> true; is_v1(<<_TimeHigh:48, 0:1, 0:1, 0:1, 1:1, % version 1 bits _TimeLow:12, 1:1, 1:1, 1:1, % ordered (future definition) variant bits _ClockSeq:13, _NodeId:48>>) -> true; is_v1(_) -> false. %%------------------------------------------------------------------------- %% @doc %% ===Get a v3 UUID.=== %% @end %%------------------------------------------------------------------------- -spec get_v3(Data :: binary()) -> uuid(). get_v3(Data) when is_binary(Data) -> <> = crypto:hash(md5, Data), B4 = (B4a bxor B4b) bxor B4c, <>. %%------------------------------------------------------------------------- %% @doc %% ===Get a v3 UUID in a particular namespace.=== %% @end %%------------------------------------------------------------------------- -spec get_v3(Namespace :: dns | url | oid | x500 | binary(), Data :: iodata()) -> uuid(). get_v3(dns, Data) -> get_v3(?UUID_NAMESPACE_DNS, Data); get_v3(url, Data) -> get_v3(?UUID_NAMESPACE_URL, Data); get_v3(oid, Data) -> get_v3(?UUID_NAMESPACE_OID, Data); get_v3(x500, Data) -> get_v3(?UUID_NAMESPACE_X500, Data); get_v3(Namespace, Data) when is_binary(Namespace) -> get_v3(erlang:iolist_to_binary([Namespace, Data])). %%------------------------------------------------------------------------- %% @doc %% ===Get a compatible v3 UUID.=== %% Do not use all bits from the checksum so that the UUID matches external %% implementations. %% @end %%------------------------------------------------------------------------- -spec get_v3_compat(Data :: binary()) -> uuid(). get_v3_compat(Data) when is_binary(Data) -> <> = crypto:hash(md5, Data), <>. %%------------------------------------------------------------------------- %% @doc %% ===Get a compatible v3 UUID in a particular namespace.=== %% Do not use all bits from the checksum so that the UUID matches external %% implementations. %% @end %%------------------------------------------------------------------------- -spec get_v3_compat(Namespace :: dns | url | oid | x500 | binary(), Data :: iodata()) -> uuid(). get_v3_compat(dns, Data) -> get_v3_compat(?UUID_NAMESPACE_DNS, Data); get_v3_compat(url, Data) -> get_v3_compat(?UUID_NAMESPACE_URL, Data); get_v3_compat(oid, Data) -> get_v3_compat(?UUID_NAMESPACE_OID, Data); get_v3_compat(x500, Data) -> get_v3_compat(?UUID_NAMESPACE_X500, Data); get_v3_compat(Namespace, Data) when is_binary(Namespace) -> get_v3_compat(erlang:iolist_to_binary([Namespace, Data])). %%------------------------------------------------------------------------- %% @doc %% ===Is the binary a v3 UUID?=== %% @end %%------------------------------------------------------------------------- -spec is_v3(Value :: any()) -> boolean(). is_v3(<<_:48, 0:1, 0:1, 1:1, 1:1, % version 3 bits _:12, 1:1, 0:1, % RFC 4122 variant bits _:62>>) -> true; is_v3(_) -> false. %%------------------------------------------------------------------------- %% @doc %% ===Get a v4 UUID (using crypto/openssl).=== %% crypto:strong_rand_bytes/1 repeats in the same way as %% RAND_bytes within OpenSSL. %% @end %%------------------------------------------------------------------------- -spec get_v4() -> uuid(). get_v4() -> get_v4(strong). -spec get_v4('strong' | 'cached' | quickrand_cache:state()) -> uuid() | {uuid(), quickrand_cache:state()}. get_v4(strong) -> <> = crypto:strong_rand_bytes(16), <>; get_v4(cached) -> <> = quickrand_cache:rand_bytes(16), <>; get_v4(Cache) when element(1, Cache) =:= quickrand_cache -> {<>, CacheNew} = quickrand_cache:rand_bytes(16, Cache), {<>, CacheNew}. %%------------------------------------------------------------------------- %% @doc %% ===Get a v4 UUID (using a 256-bit Marsaglia multiply-with-carry PRNG).=== %% quickrand:mwc256_128/1 repeats every 5.79e76 (2^255) approx. %% (see quickrand.erl for details) %% @end %%------------------------------------------------------------------------- -spec get_v4_urandom() -> uuid(). get_v4_urandom() -> % random 122 bits Rand = quickrand:mwc256_128(16#4000000000000000000000000000000) - 1, <> = <>, <>. %%------------------------------------------------------------------------- %% @doc %% ===Is the binary a v4 UUID?=== %% @end %%------------------------------------------------------------------------- -spec is_v4(Value :: any()) -> boolean(). is_v4(<<_:48, 0:1, 1:1, 0:1, 0:1, % version 4 bits _:12, 1:1, 0:1, % RFC 4122 variant bits _:62>>) -> true; is_v4(_) -> false. %%------------------------------------------------------------------------- %% @doc %% ===Get a v5 UUID.=== %% @end %%------------------------------------------------------------------------- -spec get_v5(Data :: binary()) -> uuid(). get_v5(Data) when is_binary(Data) -> <> = crypto:hash(sha, Data), B4 = ((B4a bxor B4b) bxor B4c) bxor B4d, <>. %%------------------------------------------------------------------------- %% @doc %% ===Get a v5 UUID in a particular namespace.=== %% @end %%------------------------------------------------------------------------- -spec get_v5(Namespace :: dns | url | oid | x500 | binary(), Data :: iodata()) -> uuid(). get_v5(dns, Data) -> get_v5(?UUID_NAMESPACE_DNS, Data); get_v5(url, Data) -> get_v5(?UUID_NAMESPACE_URL, Data); get_v5(oid, Data) -> get_v5(?UUID_NAMESPACE_OID, Data); get_v5(x500, Data) -> get_v5(?UUID_NAMESPACE_X500, Data); get_v5(Namespace, Data) when is_binary(Namespace) -> get_v5(erlang:iolist_to_binary([Namespace, Data])). %%------------------------------------------------------------------------- %% @doc %% ===Get a compatible v5 UUID.=== %% Do not use all bits from the checksum so that the UUID matches external %% implementations. %% @end %%------------------------------------------------------------------------- -spec get_v5_compat(Data :: binary()) -> uuid(). get_v5_compat(Data) when is_binary(Data) -> <> = crypto:hash(sha, Data), <>. %%------------------------------------------------------------------------- %% @doc %% ===Get a compatible v5 UUID in a particular namespace.=== %% Do not use all bits from the checksum so that the UUID matches external %% implementations. %% @end %%------------------------------------------------------------------------- -spec get_v5_compat(Namespace :: dns | url | oid | x500 | binary(), Data :: iodata()) -> uuid(). get_v5_compat(dns, Data) -> get_v5_compat(?UUID_NAMESPACE_DNS, Data); get_v5_compat(url, Data) -> get_v5_compat(?UUID_NAMESPACE_URL, Data); get_v5_compat(oid, Data) -> get_v5_compat(?UUID_NAMESPACE_OID, Data); get_v5_compat(x500, Data) -> get_v5_compat(?UUID_NAMESPACE_X500, Data); get_v5_compat(Namespace, Data) when is_binary(Namespace) -> get_v5_compat(erlang:iolist_to_binary([Namespace, Data])). %%------------------------------------------------------------------------- %% @doc %% ===Is the binary a v5 UUID?=== %% @end %%------------------------------------------------------------------------- -spec is_v5(Value :: any()) -> boolean(). is_v5(<<_:48, 0:1, 1:1, 0:1, 1:1, % version 5 bits _:12, 1:1, 0:1, % RFC 4122 variant bits _:62>>) -> true; is_v5(_) -> false. %%------------------------------------------------------------------------- %% @doc %% ===Convert a UUID to a list.=== %% @end %%------------------------------------------------------------------------- -spec uuid_to_list(uuid()) -> nonempty_list(non_neg_integer()). uuid_to_list(<>) -> [B1, B2, B3, B4, B5]. %%------------------------------------------------------------------------- %% @doc %% ===Convert a UUID to a string representation.=== %% @end %%------------------------------------------------------------------------- -spec uuid_to_string(Value :: uuid()) -> uuid_string_list(). uuid_to_string(Value) -> uuid_to_string(Value, standard). %%------------------------------------------------------------------------- %% @doc %% ===Convert a UUID to a string representation based on an option.=== %% @end %%------------------------------------------------------------------------- -spec uuid_to_string(Value :: uuid(), Option :: standard | nodash | list_standard | list_nodash | binary_standard | binary_nodash) -> uuid_string(). uuid_to_string(<>, standard) -> [N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32] = int_to_hex_list(Value, 32), [N01, N02, N03, N04, N05, N06, N07, N08, $-, N09, N10, N11, N12, $-, N13, N14, N15, N16, $-, N17, N18, N19, N20, $-, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32]; uuid_to_string(<>, nodash) -> int_to_hex_list(Value, 32); uuid_to_string(Value, list_standard) -> uuid_to_string(Value, standard); uuid_to_string(Value, list_nodash) -> uuid_to_string(Value, nodash); uuid_to_string(<>, binary_standard) -> [N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32] = int_to_hex_list(Value, 32), <>; uuid_to_string(<>, binary_nodash) -> [N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32] = int_to_hex_list(Value, 32), <>. %%------------------------------------------------------------------------- %% @doc %% ===Convert a string representation to a UUID.=== %% @end %%------------------------------------------------------------------------- -spec string_to_uuid(uuid_string()) -> uuid(). string_to_uuid([N01, N02, N03, N04, N05, N06, N07, N08, $-, N09, N10, N11, N12, $-, N13, N14, N15, N16, $-, N17, N18, N19, N20, $-, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32]) -> string_to_uuid(N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32); string_to_uuid([N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32]) -> string_to_uuid(N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32); string_to_uuid(<>) -> string_to_uuid(N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32); string_to_uuid(<>) -> string_to_uuid(N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32); string_to_uuid(_) -> erlang:exit(badarg). string_to_uuid(N01, N02, N03, N04, N05, N06, N07, N08, N09, N10, N11, N12, N13, N14, N15, N16, N17, N18, N19, N20, N21, N22, N23, N24, N25, N26, N27, N28, N29, N30, N31, N32) -> B01 = hex_to_int(N01, N02), B02 = hex_to_int(N03, N04), B03 = hex_to_int(N05, N06), B04 = hex_to_int(N07, N08), B05 = hex_to_int(N09, N10), B06 = hex_to_int(N11, N12), B07 = hex_to_int(N13, N14), B08 = hex_to_int(N15, N16), B09 = hex_to_int(N17, N18), B10 = hex_to_int(N19, N20), B11 = hex_to_int(N21, N22), B12 = hex_to_int(N23, N24), B13 = hex_to_int(N25, N26), B14 = hex_to_int(N27, N28), B15 = hex_to_int(N29, N30), B16 = hex_to_int(N31, N32), <>. %%------------------------------------------------------------------------- %% @doc %% ===Is the term a UUID?=== %% @end %%------------------------------------------------------------------------- -spec is_uuid(any()) -> boolean(). is_uuid(<<0:128>>) -> true; is_uuid(<<_:48, Version:4/unsigned-integer, _:12, 1:1, 0:1, % RFC 4122 variant bits _:62>>) -> (Version == 1) orelse (Version == 3) orelse (Version == 4) orelse (Version == 5); is_uuid(<<_:48, Version:4/unsigned-integer, _:12, 1:1, 1:1, 1:1, % ordered (future definition) variant bits _:61>>) -> (Version == 1); is_uuid(_) -> false. %%------------------------------------------------------------------------- %% @doc %% ===Increment the clock sequence of v1 UUID state or a UUID.=== %% Call to increment the clock sequence counter after the system clock has %% been set backwards (see the RFC). This is only necessary %% if the `os' or `warp' timestamp_type is used with a v1 UUID. %% The v3, v4 and v5 UUIDs are supported for completeness. %% @end %%------------------------------------------------------------------------- -spec increment(state() | uuid()) -> state() | uuid(). increment(<>) -> ClockSeqNext = ClockSeq + 1, ClockSeqNew = if ClockSeqNext == 16384 -> 0; true -> ClockSeqNext end, <>; increment(<>) -> ClockSeqNext = ClockSeq + 1, ClockSeqNew = if ClockSeqNext == 8192 -> 0; true -> ClockSeqNext end, <>; increment(<>) when (Version == 4) orelse (Version == 5) orelse (Version == 3) -> <> = <>, ValueNext = Value + 1, ValueNew = if ValueNext == 5316911983139663491615228241121378304 -> 1; true -> ValueNext end, <> = <>, <>; increment(#uuid_state{variant = Variant, clock_seq = ClockSeq} = State) -> ClockSeqNext = ClockSeq + 1, ClockSeqNew = if (Variant =:= rfc4122) andalso (ClockSeqNext == 16384); (Variant =:= ordered) andalso (ClockSeqNext == 8192) -> 0; true -> ClockSeqNext end, State#uuid_state{clock_seq = ClockSeqNew}. %%------------------------------------------------------------------------- %% @doc %% ===Provide a usable network interface MAC address.=== %% @end %%------------------------------------------------------------------------- -spec mac_address() -> nonempty_list(non_neg_integer()). mac_address() -> {ok, Ifs} = inet:getifaddrs(), mac_address(lists:keysort(1, Ifs)). %%------------------------------------------------------------------------- %% @doc %% ===Regression test.=== %% @end %%------------------------------------------------------------------------- -spec test() -> ok. test() -> true = uuid:is_uuid(<<0:128>>), % version 1 tests % uuidgen -t ; date % "Fri Dec 7 19:13:58 PST 2012" % (Sat Dec 8 03:13:58 UTC 2012) V1uuid1 = uuid:string_to_uuid("4ea4b020-40e5-11e2-ac70-001fd0a5484e"), "4ea4b020-40e5-11e2-ac70-001fd0a5484e" = uuid:uuid_to_string(V1uuid1, standard), <> = V1uuid1, true = uuid:is_uuid(V1uuid1), true = uuid:is_v1(V1uuid1), "2012-12-08T03:13:58.564048Z" = uuid:get_v1_datetime(V1uuid1), <> = <>, V1Time1total = (V1Time1 - 16#01b21dd213814000) div 10, V1Time1mega = V1Time1total div 1000000000000, V1Time1sec = V1Time1total div 1000000 - V1Time1mega * 1000000, V1Time1micro = V1Time1total - (V1Time1mega * 1000000 + V1Time1sec) * 1000000, {{2012, 12, 8}, {3, 13, 58}} = calendar:now_to_datetime({V1Time1mega, V1Time1sec, V1Time1micro}), % max version 1 timestamp: "5236-03-31T21:21:00.684697Z" = uuid:get_v1_datetime(<<16#ffffffff:32, 16#ffff:16, 0:1, 0:1, 0:1, 1:1, 16#fff:12, 1:1, 0:1, 0:14, 0:48>>), % $ python % >>> import uuid % >>> import time % >>> uuid.uuid1().hex;time.time() % '50d15f5c40e911e2a0eb001fd0a5484e' % 1354938160.1998589 V1uuid2 = uuid:string_to_uuid("50d15f5c40e911e2a0eb001fd0a5484e"), "50d15f5c40e911e2a0eb001fd0a5484e" = uuid:uuid_to_string(V1uuid2, nodash), "2012-12-08T03:42:40.199254Z" = uuid:get_v1_datetime(V1uuid2), <> = V1uuid2, true = uuid:is_v1(V1uuid2), <> = <>, V1Time2total = (V1Time2 - 16#01b21dd213814000) div 10, V1Time2Amega = V1Time2total div 1000000000000, V1Time2Asec = V1Time2total div 1000000 - V1Time2Amega * 1000000, V1Time2Amicro = V1Time2total - (V1Time2Amega * 1000000 + V1Time2Asec) * 1000000, V1Time2B = 1354938160.1998589, V1Time2Bmega = erlang:trunc(V1Time2B / 1000000), V1Time2Bsec = erlang:trunc(V1Time2B - V1Time2Bmega * 1000000), V1Time2Bmicro = erlang:trunc(V1Time2B * 1000000 - (V1Time2Bmega * 1000000 + V1Time2Bsec) * 1000000), true = (V1Time2Amega == V1Time2Bmega), true = (V1Time2Asec == V1Time2Bsec), true = (V1Time2Amicro < V1Time2Bmicro) and ((V1Time2Amicro + 605) == V1Time2Bmicro), true = V1ClockSeq1 /= V1ClockSeq2, true = V1NodeId1 == V1NodeId2, {V1uuid3, _} = uuid:get_v1(uuid:new(self(), erlang)), V1uuid3timeB = uuid:get_v1_time(erlang), V1uuid3timeA = uuid:get_v1_time(V1uuid3), true = (V1uuid3timeA < V1uuid3timeB) and ((V1uuid3timeA + 1000) > V1uuid3timeB), true = is_number(uuid:get_v1_time( uuid:string_to_uuid("3ff0fc1e-c23b-11e2-b8a0-38607751fca5"))), true = is_number(uuid:get_v1_time( uuid:string_to_uuid("67ff79a6-c23b-11e2-b374-38607751fca5"))), true = is_number(uuid:get_v1_time( uuid:string_to_uuid("7134eede-c23b-11e2-a4a7-38607751fca5"))), true = is_number(uuid:get_v1_time( uuid:string_to_uuid("717003c0-c23b-11e2-83a4-38607751fca5"))), {V1uuid4, _} = uuid:get_v1(uuid:new(self(), os)), V1uuid4timeB = uuid:get_v1_time(os), V1uuid4timeA = uuid:get_v1_time(V1uuid4), true = (V1uuid4timeA < V1uuid4timeB) and ((V1uuid4timeA + 1000) > V1uuid4timeB), V1State0 = uuid:new(self()), {V1uuid5, V1State1} = uuid:get_v1(V1State0), {V1uuid6, V1State2} = uuid:get_v1(V1State1), {V1uuid7, V1State3} = uuid:get_v1(V1State2), {V1uuid8, V1State4} = uuid:get_v1(V1State3), {V1uuid9, V1State5} = uuid:get_v1(V1State4), {V1uuid10, V1State6} = uuid:get_v1(V1State5), {V1uuid11, _} = uuid:get_v1(V1State6), V1uuidL0 = [V1uuid5, V1uuid6, V1uuid7, V1uuid8, V1uuid9, V1uuid10, V1uuid11], V1uuidL1 = [V1uuid11, V1uuid9, V1uuid8, V1uuid7, V1uuid6, V1uuid10, V1uuid5], true = V1uuidL0 == lists:usort(V1uuidL1), % version 1 ordered variant V1uuidOrdered0 = uuid:string_to_uuid("1ea55423c2f912f6e847e7a9663898ef"), V1uuidOrdered1 = uuid:string_to_uuid("1ea554248aa31464e847e7a9663898ef"), true = V1uuidOrdered1 > V1uuidOrdered0, true = uuid:is_v1(V1uuidOrdered0), true = uuid:is_v1(V1uuidOrdered1), V1uuidOrdered2 = uuid:increment(V1uuidOrdered1), true = uuid:is_v1(V1uuidOrdered2), <<_:48, 0:1, 0:1, 0:1, 1:1, % version 1 bits _:12, 1:1, 1:1, 1:1, % ordered (future definition) variant bits _:13, _:48>> = V1uuidOrdered2, V1OrderedState0 = uuid:new(self(), [{variant, ordered}]), {V1uuidOrdered3, _} = uuid:get_v1(V1OrderedState0), <<_:48, 0:1, 0:1, 0:1, 1:1, % version 1 bits _:12, 1:1, 1:1, 1:1, % ordered (future definition) variant bits _:13, _:48>> = V1uuidOrdered3, % version 3 tests % $ python % >>> import uuid % >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'test').hex % '45a113acc7f230b090a5a399ab912716' V3uuid1 = uuid:string_to_uuid("45a113acc7f230b090a5a399ab912716"), "45a113acc7f230b090a5a399ab912716" = uuid:uuid_to_string(V3uuid1, nodash), <> = V3uuid1, true = uuid:is_v3(V3uuid1), V3uuid2 = uuid:get_v3(?UUID_NAMESPACE_DNS, <<"test">>), true = (V3uuid2 == uuid:get_v3(dns, <<"test">>)), <> = V3uuid2, true = uuid:is_v3(V3uuid2), true = ((V3uuid1A == V3uuid2A) and (V3uuid1B == V3uuid2B) and (V3uuid1C == V3uuid2C)), % check fails: % since the python uuid discards bits from MD5 while this module % bitwise xor the middle bits to utilize the whole checksum false = (V3uuid1D == V3uuid2D), % replicate the same UUID value used in other implementations % where bits are discarded from the checksum V3uuid1 = uuid:get_v3_compat(?UUID_NAMESPACE_DNS, <<"test">>), true = (uuid:get_v3_compat(dns, <<"test1">>) == uuid:string_to_uuid("c501822b22a837ff91a99545f4689a3d")), true = (uuid:get_v3_compat(dns, <<"test2">>) == uuid:string_to_uuid("f191764306b23e6dab770a5044067d0a")), true = (uuid:get_v3_compat(dns, <<"test3">>) == uuid:string_to_uuid("bf7f1e5a6b28310c8f9ef815dbd56fb7")), true = (uuid:get_v3_compat(dns, <<"test4">>) == uuid:string_to_uuid("fe584e2496c83d2d8b39f1cc6a877f72")), % version 4 tests % uuidgen -r V4uuid1 = uuid:string_to_uuid("ffe8b758-a5dc-4bf4-9eb0-878e010e8df7"), "ffe8b758-a5dc-4bf4-9eb0-878e010e8df7" = uuid:uuid_to_string(V4uuid1, standard), <> = V4uuid1, true = uuid:is_v4(V4uuid1), % $ python % >>> import uuid % >>> uuid.uuid4().hex % 'cc9769818fe747398e2422e99fee2753' V4uuid2 = uuid:string_to_uuid("cc9769818fe747398e2422e99fee2753"), "cc9769818fe747398e2422e99fee2753" = uuid:uuid_to_string(V4uuid2, nodash), <> = V4uuid2, true = uuid:is_v4(V4uuid2), V4uuid3 = uuid:get_v4(strong), <<_:48, 0:1, 1:1, 0:1, 0:1, % version 4 bits _:12, 1:1, 0:1, % RFC 4122 variant bits _:14, _:48>> = V4uuid3, true = uuid:is_v4(V4uuid3), true = (V4Rand1A /= V4Rand2A), true = (V4Rand1B /= V4Rand2B), true = (V4Rand1C /= V4Rand2C), true = (V4Rand1D /= V4Rand2D), true = V4uuid3 /= uuid:increment(V4uuid3), % version 5 tests % $ python % >>> import uuid % >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'test').hex % '4be0643f1d98573b97cdca98a65347dd' V5uuid1 = uuid:string_to_uuid("4be0643f1d98573b97cdca98a65347dd"), "4be0643f1d98573b97cdca98a65347dd" = uuid:uuid_to_string(V5uuid1, nodash), <> = V5uuid1, true = uuid:is_v5(V5uuid1), V5uuid2 = uuid:get_v5(?UUID_NAMESPACE_DNS, <<"test">>), true = (V5uuid2 == uuid:get_v5(dns, <<"test">>)), <> = V5uuid2, true = uuid:is_v5(V5uuid2), true = ((V5uuid1A == V5uuid2A) and (V5uuid1B == V5uuid2B) and (V5uuid1C == V5uuid2C)), % check fails: % since the python uuid discards bits from SHA while this module % bitwise xor the remaining bits to utilize the whole checksum false = (V5uuid1D == V5uuid2D), % replicate the same UUID value used in other implementations % where bits are discarded from the checksum V5uuid1 = uuid:get_v5_compat(?UUID_NAMESPACE_DNS, <<"test">>), true = (uuid:get_v5_compat(dns, <<"test1">>) == uuid:string_to_uuid("86e3aed315535d238d612286215e65f1")), true = (uuid:get_v5_compat(dns, <<"test2">>) == uuid:string_to_uuid("6eabff02c9685cbcbc7f3b672928a761")), true = (uuid:get_v5_compat(dns, <<"test3">>) == uuid:string_to_uuid("20ca53afd04c58a2a8b3d02b9e414e80")), true = (uuid:get_v5_compat(dns, <<"test4">>) == uuid:string_to_uuid("3e673fdc1a4f5b168890dbe7e763f7b5")), ok. %%%------------------------------------------------------------------------ %%% Private functions %%%------------------------------------------------------------------------ -compile({inline, [{timestamp,1}, {timestamp,2}, {timestamp_nanoseconds,1}, {int_to_dec,1}, {int_to_hex,1}, {hex_to_int,1}]}). timestamp(TimestampTypeInternal) -> timestamp_nanoseconds(TimestampTypeInternal) div 100. -ifdef(ERLANG_OTP_VERSION_18_FEATURES). timestamp_type_erlang() -> % Erlang >= 18.0 true = erlang:function_exported(erlang, system_time, 0), erlang_timestamp. -ifdef(ERLANG_OTP_VERSION_20_FEATURES). timestamp_nanoseconds(erlang_timestamp) -> erlang:system_time(nanosecond); timestamp_nanoseconds(os) -> os:system_time(nanosecond); timestamp_nanoseconds(warp) -> erlang:system_time(nanosecond). -else. timestamp_nanoseconds(erlang_timestamp) -> erlang:system_time(nano_seconds); timestamp_nanoseconds(os) -> os:system_time(nano_seconds); timestamp_nanoseconds(warp) -> erlang:system_time(nano_seconds). -endif. timestamp(erlang_timestamp, TimestampLast) -> TimestampNext = timestamp(erlang_timestamp), if TimestampNext > TimestampLast -> TimestampNext; true -> TimestampLast + 1 end; timestamp(os, _) -> timestamp(os); timestamp(warp, _) -> timestamp(warp). -else. timestamp_type_erlang() -> % Erlang < 18.0 false = erlang:function_exported(erlang, system_time, 0), erlang_now. timestamp_nanoseconds(erlang_now) -> {MegaSeconds, Seconds, MicroSeconds} = erlang:now(), ((MegaSeconds * 1000000 + Seconds) * 1000000 + MicroSeconds) * 1000; timestamp_nanoseconds(os) -> {MegaSeconds, Seconds, MicroSeconds} = os:timestamp(), ((MegaSeconds * 1000000 + Seconds) * 1000000 + MicroSeconds) * 1000. timestamp(erlang_now, _) -> timestamp(erlang_now); timestamp(os, _) -> timestamp(os). -endif. int_to_dec_list(I, N) when is_integer(I), I >= 0 -> int_to_dec_list([], I, 1, N). int_to_dec_list(L, I, Count, N) when I < 10 -> int_to_list_pad([int_to_dec(I) | L], N - Count); int_to_dec_list(L, I, Count, N) -> int_to_dec_list([int_to_dec(I rem 10) | L], I div 10, Count + 1, N). int_to_hex_list(I, N) when is_integer(I), I >= 0 -> int_to_hex_list([], I, 1, N). int_to_list_pad(L, 0) -> L; int_to_list_pad(L, Count) -> int_to_list_pad([$0 | L], Count - 1). int_to_hex_list(L, I, Count, N) when I < 16 -> int_to_list_pad([int_to_hex(I) | L], N - Count); int_to_hex_list(L, I, Count, N) -> int_to_hex_list([int_to_hex(I rem 16) | L], I div 16, Count + 1, N). int_to_dec(I) when 0 =< I, I =< 9 -> I + $0. int_to_hex(I) when 0 =< I, I =< 9 -> I + $0; int_to_hex(I) when 10 =< I, I =< 15 -> (I - 10) + $a. hex_to_int(C1, C2) -> hex_to_int(C1) * 16 + hex_to_int(C2). hex_to_int(C) when $0 =< C, C =< $9 -> C - $0; hex_to_int(C) when $A =< C, C =< $F -> C - $A + 10; hex_to_int(C) when $a =< C, C =< $f -> C - $a + 10. mac_address([]) -> [0, 0, 0, 0, 0, 0]; mac_address([{_, L} | Rest]) -> case lists:keyfind(hwaddr, 1, L) of false -> mac_address(Rest); {hwaddr, MAC} -> case lists:sum(MAC) of 0 -> mac_address(Rest); _ -> MAC end end. -ifdef(ERLANG_OTP_VERSION_18_FEATURES). pseudo_random(N) -> % assuming exsp/exsplus for 58 bits, period 8.31e34 rand:uniform(N). -else. pseudo_random(N) -> % period 2.78e13 random:uniform(N). -endif. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -ifdef(CLOUDI_TEST_TIMEOUT). -define(TEST_TIMEOUT, ?CLOUDI_TEST_TIMEOUT). % seconds -else. -define(TEST_TIMEOUT, 10). % seconds -endif. module_test_() -> {timeout, ?TEST_TIMEOUT, [ {"internal tests", ?_assertEqual(ok, test())} ]}. -endif.