-
Notifications
You must be signed in to change notification settings - Fork 1
/
zuuid_man.erl
383 lines (306 loc) · 12.7 KB
/
zuuid_man.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
%%% @doc
%%% zUUID state manager process.
%%%
%%% This process manages the state of time-based generation data for UUID
%%% versions 1 and 2 and implements measures to prevent generation of
%%% duplicate UUIDs in the case of very high frequency calls to {@link zuuid:v1/0},
%%% {@link zuuid:v2/0} or {@link zuuid:v2/1}.
%%%
%%% Starting the zUUID application with zuuid:start/0 is not necessary if only
%%% using version 3, 4 or 5 UUIDs, or for using the UUID manipulation functions
%%% in the uuid module.
%%%
%%% On startup this process will initialize itself with random data for the
%%% clock sequence, node/MAC address, posix UID and posix location/GID values.
%%% If custom values are desired for any of these state attributes (such as
%%% the actual primary MAC address used by the machine, actual posix UID, or
%%% some other deliberate identifying data) they can be set after this
%%% process has started by using {@link zuuid:config/1}.
%%% @end
-module(zuuid_man).
-author("Craig Everett <zxq9@zxq9.com>").
-behavior(gen_server).
-export([config/1, start_link/0, check_offset/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%%% State record
-record(s, {clock_seq = random_clock() :: zuuid:clock_seq(),
clock_adj = 1 :: non_neg_integer(),
node = random_mac() :: zuuid:ieee802mac(),
posix_id = random_uid() :: zuuid:posix_id(),
local_id = random_lid() :: zuuid:local_id(),
last_v1 = zuuid:nil() :: zuuid:uuid(),
last_v2 = zuuid:nil() :: zuuid:uuid()}).
%%% Types
-type state() :: #s{}.
%%% Constants
% From RFC 4122:
% ```
% 4.1.4. Timestamp
% The timestamp is a 60-bit value. For UUID version 1, this is
% represented by Coordinated Universal Time (UTC) as a count of 100-
% nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of
% Gregorian reform to the Christian calendar).
% '''
% For a more detailed explanation of this magical constant,
% see definition of check_offset/0.
-define(OFFSET, 122192928000000000).
%%% Interface
-spec config(Value) -> Result
when Value :: {clock_seq, random | zuuid:clock_seq()}
| {node, random | zuuid:ieee802mac() | bad_mac}
| {posix_id, random | zuuid:posix_id()}
| {local_id, random | zuuid:local_id()},
Result :: ok
| {error, Reason},
Reason :: bad_mac.
%% @private
%% Allows zuuid application to be configured after startup with any desired
%% values that would affect generation of version 1 or 2 UUIDs (versions 3, 4
%% and 5 do not use system state information to generate their result).
%%
%% Accepts a value of `{node, bad_mac}' to permit compositions of the
%% following form without crashing the state management process on bad
%% input:
%% ```
%% zuuid:config({node, zuuid:read_mac(SomeString)})
%% '''
%%
%% This function accepts only explicit attribute arguments as a filter to crash
%% the caller in the event of an illegal call instead of crashing this state manager.
%%
%% NOTE: The export from this module should not be called directly.
%% Use zuuid:config/1 instead.
%% @see zuuid:config/1.
config({clock_seq, Value}) ->
gen_server:cast(?MODULE, {config, {clock_seq, Value}});
config({node, bad_mac}) ->
{error, bad_mac};
config({node, Value}) ->
gen_server:cast(?MODULE, {config, {node, Value}});
config({posix_id, Value}) ->
gen_server:cast(?MODULE, {config, {posix_id, Value}});
config({local_id, Value}) ->
gen_server:cast(?MODULE, {config, {local_id, Value}}).
%%% Startup
-spec start_link() -> {ok, pid()} | {error, term()}.
%% @private
%% Startup function -- intended to be called by zuuid_sup.
%%
%% Error conditions are documented in the gen_server module:
%% http://zxq9.com/erlang/docs/reg/18.0/lib/stdlib-2.5/doc/html/gen_server.html#start_link-4
start_link() ->
start_link(none).
-spec start_link(none) -> {ok, pid()} | {error, term()}.
%% @private
%% Alternative pre-configured startup, currently only ever passed 'none' as an argument.
%%
%% Error conditions are documented in the gen_server module:
%% http://zxq9.com/erlang/docs/reg/18.0/lib/stdlib-2.5/doc/html/gen_server.html#start_link-4
start_link(Args) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []).
-spec init(term()) -> {ok, state()}.
%% @doc
%% gen_server callback for startup.
%%
%% zuuid_man initializes every time with a randomized internal state for
%% generation of version 1 and 2 UUIDs. Users are advised to configure
%% the uuid state manager after startup to customize the state if desired.
%% @see zuuid:config/1.
init(_) ->
{ok, #s{}}.
%%% gen_server
%% @private
handle_call(v1, _, State) ->
{Result, NewState} = v1(State),
{reply, Result, NewState};
handle_call(v2, _, State = #s{posix_id = PosixID, local_id = LocalID}) ->
{Result, NewState} = v2(PosixID, LocalID, State),
{reply, Result, NewState};
handle_call({v2, PosixID, LocalID}, _, State) ->
{Result, NewState} = v2(PosixID, LocalID, State),
{reply, Result, NewState};
%% Replace this clause with a call to a logger if your system is using one. In most
%% cases the following call to io:format/2 will go nowhere (STDOUT is probably not
%% connected to anything useful).
handle_call(Unexpected, From, State) ->
ok = io:format("~p: Unexpected call from ~p: ~tp~n", [self(), From, Unexpected]),
{noreply, State}.
%% @private
%% Should only be receiving casts generated by zuuid:config/1 -- anything else should
%% drop a notification.
handle_cast({config, Value}, State) ->
NewState = do_config(Value, State),
{noreply, NewState};
%% Replace this clause with a call to a logger if your system is using one. In most
%% cases the following call to io:format/2 will go nowhere (STDOUT is probably not
%% connected to anything useful).
handle_cast(Unexpected, State) ->
ok = io:format("~p: Unexpected cast: ~tp~n", [self(), Unexpected]),
{noreply, State}.
%% @private
%% Normally this process should not be receiving any non-OTP messages.
%% Replace this clause with a call to a logger if your system is using one. In most
%% cases the following call to io:format/2 will go nowhere (STDOUT is probably not
%% connected to anything useful).
handle_info(Unexpected, State) ->
ok = io:format("~p: Unexpected info: ~tp~n", [self(), Unexpected]),
{noreply, State}.
%% @private
terminate(_, _) ->
ok.
%% @private
code_change(_, State, _) ->
{ok, State}.
%%% UUID generation
%% V1
-spec v1(State) -> {UUID, NewState}
when State :: state(),
UUID :: zuuid:uuid(),
NewState :: state().
%% Generate a version 1 UUID in accordance with RFC 4122.
%% (http://www.ietf.org/rfc/rfc4122.txt)
%%
%% This function checks that the last generated value is not the same as the current
%% one (single-history duplicate detection), generating a new one with an updated
%% clock sequence if it is.
v1(State = #s{clock_seq = Seq, clock_adj = Adj, node = Node, last_v1 = Last}) ->
case gen_v1(Seq, Node) of
Last = {uuid, <<Pref:66, _:62>>} ->
UUID = {uuid, <<Pref:66, (Seq + Adj):14, Node/binary>>},
{UUID, State#s{clock_adj = Adj + 1, last_v1 = UUID}};
UUID ->
{UUID, State#s{clock_adj = 1, last_v1 = UUID}}
end.
-spec gen_v1(zuuid:clock_seq(), <<_:48>>) -> zuuid:uuid().
%% Assembly of version 1 UUID.
gen_v1(ClockSeq, Node) ->
GregorianInterval = ?OFFSET + erlang:system_time(nano_seconds) div 100,
<<High:12, Mid:16, Low:32>> = <<GregorianInterval:60>>,
Variant = 2, % Indicates RFC 4122
Version = 1, % UUID version number
{uuid, <<Low:32, Mid:16, Version:4, High:12, Variant:2, ClockSeq:14, Node/binary>>}.
%% V2
-spec v2(PosixID, LocalID, State) -> {UUID, NewState}
when PosixID :: zuuid:posix_id(),
LocalID :: zuuid:local_id(),
State :: state(),
UUID :: zuuid:uuid(),
NewState :: state().
%% Generate a version 2 (DEC Security) UUID using stored ID values.
%%
%% This function checks that the last generated value is not the same as the current
%% one (single-history duplicate detection), generating a new one with an updated
%% clock sequence if it is.
v2(PosixID,
LocalID,
State = #s{clock_seq = Seq, clock_adj = Adj, node = Node, last_v2 = Last}) ->
case gen_v2(PosixID, LocalID, Seq, Node) of
Last = {uuid, <<Pref:74, _:54>>} ->
<<_:8, Adjusted:6>> = <<(Seq + Adj):14>>,
UUID = {uuid, <<Pref:74, Adjusted:6, Node/binary>>},
{UUID, State#s{clock_adj = Adj + 1, last_v2 = UUID}};
UUID ->
{UUID, State#s{clock_adj = 1, last_v2 = UUID}}
end.
-spec gen_v2(PosixID, LocalID, ClockSeq, MAC) -> UUID
when PosixID :: zuuid:posix_id(),
LocalID :: zuuid:local_id(),
ClockSeq :: zuuid:clock_seq(),
MAC :: zuuid:ieee802mac(),
UUID :: zuuid:uuid().
%% Assembly of version 2 UUID.
gen_v2(PosixID, LocalID, ClockSeq, Node) ->
GregorianInterval = ?OFFSET + erlang:system_time(nano_seconds) div 100,
<<_:32, Time:28>> = <<GregorianInterval:60>>,
<<PosixA:20, PosixB:12>> = <<PosixID:32>>,
<<_:8, Seq:6>> = <<ClockSeq:14>>,
Variant = 2, % Indicates RFC 4122
Version = 2, % UUID version number
{uuid, <<Time:28, PosixA:20, Version:4,
PosixB:12, Variant:2, LocalID:8, Seq:6, Node/binary>>}.
%%% State Configuration
-spec do_config(Value, State) -> NewState
when Value :: {clock_seq, random | zuuid:clock_seq()}
| {node, random | zuuid:ieee802mac()}
| {posix_id, random | zuuid:posix_id()}
| {local_id, random | zuuid:local_id()},
State :: state(),
NewState :: state().
do_config({Attribute, random}, State) ->
case Attribute of
clock_seq -> State#s{clock_seq = random_clock()};
node -> State#s{node = random_mac()};
posix_id -> State#s{posix_id = random_uid()};
local_id -> State#s{local_id = random_lid()};
_ -> State
end;
do_config({clock_seq, Seq}, State) ->
State#s{clock_seq = Seq};
do_config({node, MAC}, State) ->
State#s{node = MAC};
do_config({posix_id, ID}, State) ->
State#s{posix_id = ID};
do_config({local_id, ID}, State) ->
State#s{local_id = ID};
do_config(_, State) ->
State.
%%% Utilities
-spec check_offset() -> true.
%% @doc
%% An explanation and hard-coded test of the magic constant macro ?OFFSET,
%% which defines the difference in nanoseconds between the RFC 4122 accounting
%% date for UUID generation and the beginning of the Unix epoch.
check_offset() ->
Greg = calendar:datetime_to_gregorian_seconds({{1582, 10, 15}, {0, 0, 0}}),
Unix = calendar:datetime_to_gregorian_seconds({{1970, 01, 01}, {0, 0, 0}}),
Interval = Unix - Greg,
Offset = Interval * 10000000,
ok = io:format("Seconds from 0AD to 1582-10-15T00:00:00Z: ~p~n"
"Seconds from 0AD to 1970-01-01T00:00:00Z: ~p~n"
"Interval between 1582-10-15 and 1970-01-01 in seconds: ~p~n"
"Offset interval in nanoseconds: ~p~n",
[Greg, Unix, Interval, Offset]),
?OFFSET == Offset.
-spec random_mac() -> zuuid:ieee802mac().
%% @private
%% Generate a random IEEE 802 MAC address in compliance with RFC 4122.
%%
%% This function will always be called automatically when zuuid:start/0
%% is called the first time. The IEEE 802 broadcast-bit is set on MAC
%% addresses returned by this function, so they should never collide with
%% actual addresses pulled from hardware components.
%%
%% To convert a hardware address in hex string notation use `read_mac/1'.
random_mac() ->
<<A:7, _:1, B:40>> = crypto:strong_rand_bytes(6),
BroadcastBit = 1, % RFC 4122 requires this be set for randomized MACs
<<A:7, BroadcastBit:1, B:40>>.
-spec random_clock() -> zuuid:clock_seq().
%% @private
%% Generate a random 14-bit clock sequence.
%%
%% This function will always be called automatically when zuuid:start/0
%% is called the first time.
random_clock() ->
<<_:2, ClockSeq:14>> = crypto:strong_rand_bytes(2),
ClockSeq.
-spec random_uid() -> zuuid:posix_id().
%% @private
%% Generate a random 4-byte value for use as POSIX UID in version 2 UUID generation.
%%
%% This function will always be called automatically when zuuid:start/0
%% is called the first time.
random_uid() ->
<<ID:32>> = crypto:strong_rand_bytes(4),
ID.
-spec random_lid() -> zuuid:local_id().
%% @private
%% Generate a random 8-bit value for use as POSIX Group/Local ID value for use
%% in version 2 UUID generation.
%%
%% This function will always be called automatically when zuuid:start/0
%% is called the first time.
random_lid() ->
<<ID:8>> = crypto:strong_rand_bytes(1),
ID.