1
+ require " time"
2
+ require " io"
3
+
4
+ {% if flag?(:without_openssl ) % }
5
+ require " crystal/digest/sha1"
6
+ require " crystal/digest/md5"
7
+ {% else % }
8
+ require " digest/sha1"
9
+ require " digest/md5"
10
+ {% end % }
11
+
1
12
# Represents a UUID (Universally Unique IDentifier).
2
13
#
3
14
# NOTE: To use `UUID`, you must explicitly import it with `require "uuid"`
@@ -10,7 +21,7 @@ struct UUID
10
21
Unknown
11
22
# Reserved by the NCS for backward compatibility.
12
23
NCS
13
- # Reserved for RFC4122 Specification (default).
24
+ # Reserved for RFC 4122 Specification (default).
14
25
RFC4122
15
26
# Reserved by Microsoft for backward compatibility.
16
27
Microsoft
@@ -22,7 +33,7 @@ struct UUID
22
33
enum Version
23
34
# Unknown version.
24
35
Unknown = 0
25
- # Date-time and MAC address.
36
+ # Date-time and NodeID address.
26
37
V1 = 1
27
38
# DCE security.
28
39
V2 = 2
@@ -34,6 +45,31 @@ struct UUID
34
45
V5 = 5
35
46
end
36
47
48
+ # A Domain represents a Version 2 domain (DCE security).
49
+ enum Domain
50
+ Person = 0
51
+ Group = 1
52
+ Org = 2
53
+ end
54
+
55
+ # MAC address to be used as NodeID.
56
+ alias MAC = UInt8 [6 ]
57
+
58
+ # Namespaces as defined per in the RFC 4122 Appendix C.
59
+ #
60
+ # They are used with the functions `v3` amd `v5` to generate
61
+ # a `UUID` based on a `name`.
62
+ module Namespace
63
+ # A UUID is generated using the provided `name`, which is assumed to be a fully qualified domain name.
64
+ DNS = UUID .new(" 6ba7b810-9dad-11d1-80b4-00c04fd430c8" )
65
+ # A UUID is generated using the provided `name`, which is assumed to be a URL.
66
+ URL = UUID .new(" 6ba7b811-9dad-11d1-80b4-00c04fd430c8" )
67
+ # A UUID is generated using the provided `name`, which is assumed to be an ISO OID.
68
+ OID = UUID .new(" 6ba7b812-9dad-11d1-80b4-00c04fd430c8" )
69
+ # A UUID is generated using the provided `name`, which is assumed to be a X.500 DN in DER or a text output format.
70
+ X500 = UUID .new(" 6ba7b814-9dad-11d1-80b4-00c04fd430c8" )
71
+ end
72
+
37
73
@bytes : StaticArray (UInt8 , 16 )
38
74
39
75
# Generates UUID from *bytes*, applying *version* and *variant* to the UUID if
@@ -176,6 +212,115 @@ struct UUID
176
212
new(new_bytes, variant, version)
177
213
end
178
214
215
+ # Generates RFC 4122 v1 UUID.
216
+ #
217
+ # The traditional method for generating a `node_id` involves using the machine’s MAC address.
218
+ # However, this approach is only effective if there is only one process running on the machine
219
+ # and if privacy is not a concern. In modern languages, the default is to prioritize security
220
+ # and privacy. Therefore, a pseudo-random `node_id` is generated as described in section 4.5 of
221
+ # the RFC.
222
+ #
223
+ # The sequence number `clock_seq` is used to generate the UUID. This number should be
224
+ # monotonically increasing, with only 14 bits of the clock sequence being used effectively.
225
+ # The clock sequence should be stored in a stable location, such as a file. If it is not
226
+ # stored, a random value is used by default. If not provided the current time milliseconds
227
+ # are used. In case the traditional MAC address based approach should be taken the
228
+ # `node_id` can be provided. Otherwise secure random is used.
229
+ def self.v1 (* , clock_seq : UInt16 ? = nil , node_id : MAC ? = nil ) : self
230
+ tl = Time .local
231
+ now = (tl.to_unix_ns / 100 ).to_u64 + 122192928000000000
232
+ seq = ((clock_seq || (tl.nanosecond/ 1000000 ).to_u16) & 0x3fff ) | 0x8000
233
+
234
+ time_low = UInt32 .new(now & 0xffffffff )
235
+ time_mid = UInt16 .new((now >> 32 ) & 0xffff )
236
+ time_hi = UInt16 .new((now >> 48 ) & 0x0fff )
237
+ time_hi |= 0x1000 # Version 1
238
+
239
+ uuid = uninitialized UInt8 [16 ]
240
+ IO ::ByteFormat ::BigEndian .encode(time_low, uuid.to_slice[0 ..3])
241
+ IO ::ByteFormat ::BigEndian .encode(time_mid, uuid.to_slice[4 ..5])
242
+ IO ::ByteFormat ::BigEndian .encode(time_hi, uuid.to_slice[6 ..7])
243
+ IO ::ByteFormat ::BigEndian .encode(seq, uuid.to_slice[8 ..9])
244
+
245
+ if node_id
246
+ 6 .times do |i |
247
+ uuid.to_slice[10 + i] = node_id[i]
248
+ end
249
+ else
250
+ Random ::Secure .random_bytes(uuid.to_slice[10 ..15])
251
+ # set multicast bit as recommended per section 4.5 of the RFC 4122 spec
252
+ # to not conflict with real MAC addresses
253
+ uuid[10 ] |= 0x01 _u8
254
+ end
255
+
256
+ new(uuid, version: UUID ::Version ::V1 , variant: UUID ::Variant ::RFC4122 )
257
+ end
258
+
259
+ # Generates RFC 4122 v2 UUID.
260
+ #
261
+ # Version 2 UUIDs are generated using the current time, the local machine’s MAC address,
262
+ # and the local user or group ID. However, they are not widely used due to their limitations.
263
+ # For a given domain/id pair, the same token may be returned for a duration of up to 7 minutes
264
+ # and 10 seconds.
265
+ #
266
+ # The `id` depends on the `domain`, for the `Domain::Person` usually the local user id (uid) is
267
+ # used, for `Domain::Group` usually the local group id (gid) is used. In case the traditional
268
+ # MAC address based approach should be taken the `node_id` can be provided. Otherwise secure
269
+ # random is used.
270
+ def self.v2 (domain : Domain , id : UInt32 , node_id : MAC ? = nil ) : self
271
+ uuid = v1(node_id: node_id).bytes
272
+ uuid[6 ] = (uuid[6 ] & 0x0f ) | 0x20 # Version 2
273
+ uuid[9 ] = domain.to_u8
274
+ IO ::ByteFormat ::BigEndian .encode(id, uuid.to_slice[0 ..3])
275
+ new(uuid, version: UUID ::Version ::V2 , variant: UUID ::Variant ::RFC4122 )
276
+ end
277
+
278
+ # Generates RFC 4122 v3 UUID using the `name` to generate the UUID, it can be a string of any size.
279
+ # The `namespace` specifies the type of the name, usually one of `Namespace`.
280
+ def self.v3 (name : String , namespace : UUID ) : self
281
+ klass = {% if flag?(:without_openssl ) % }::Crystal ::Digest ::MD5 {% else % }::Digest ::MD5 {% end % }
282
+ hash = klass.digest do |ctx |
283
+ ctx.update namespace.bytes
284
+ ctx.update name
285
+ end
286
+ new(hash[0 ...16], version: UUID ::Version ::V3 , variant: UUID ::Variant ::RFC4122 )
287
+ end
288
+
289
+ # Generates RFC 4122 v4 UUID.
290
+ #
291
+ # It is strongly recommended to use a cryptographically random source for
292
+ # *random*, such as `Random::Secure`.
293
+ def self.v4 (random r : Random = Random ::Secure ) : self
294
+ random(r)
295
+ end
296
+
297
+ # Generates RFC 4122 v5 UUID using the `name` to generate the UUID, it can be a string of any size.
298
+ # The `namespace` specifies the type of the name, usually one of `Namespace`.
299
+ def self.v5 (name : String , namespace : UUID ) : self
300
+ klass = {% if flag?(:without_openssl ) % }::Crystal ::Digest ::SHA1 {% else % }::Digest ::SHA1 {% end % }
301
+ hash = klass.digest do |ctx |
302
+ ctx.update namespace.bytes
303
+ ctx.update name
304
+ end
305
+ new(hash[0 ...16], version: UUID ::Version ::V5 , variant: UUID ::Variant ::RFC4122 )
306
+ end
307
+
308
+ {% for name in %w(DNS URL OID X500) .map(& .id) % }
309
+ # Generates RFC 4122 v3 UUID with the `Namespace::{{ name }}`.
310
+ #
311
+ # * `name`: The name used to generate the UUID, it can be a string of any size.
312
+ def self.v3_ {{ name.downcase }}(name : String )
313
+ v3(name, Namespace ::{{ name }})
314
+ end
315
+
316
+ # Generates RFC 4122 v5 UUID with the `Namespace::{{ name }}`.
317
+ #
318
+ # * `name`: The name used to generate the UUID, it can be a string of any size.
319
+ def self.v5_ {{ name.downcase }}(name : String )
320
+ v5(name, Namespace ::{{ name }})
321
+ end
322
+ {% end % }
323
+
179
324
# Generates an empty UUID.
180
325
#
181
326
# ```
0 commit comments