Skip to content

Commit 008d76a

Browse files
threezHertzDevil
andauthored
Add UUID.v1, .v2, .v3, .v4, .v5 (#13693)
Co-authored-by: Quinton Miller <nicetas.c@gmail.com>
1 parent 9e84f6e commit 008d76a

File tree

3 files changed

+238
-3
lines changed

3 files changed

+238
-3
lines changed

.github/workflows/wasm32.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
rm wasm32-wasi-libs.tar.gz
4141
4242
- name: Build spec/wasm32_std_spec.cr
43-
run: bin/crystal build spec/wasm32_std_spec.cr -o wasm32_std_spec.wasm --target wasm32-wasi -Duse_pcre
43+
run: bin/crystal build spec/wasm32_std_spec.cr -o wasm32_std_spec.wasm --target wasm32-wasi -Duse_pcre -Dwithout_openssl
4444
env:
4545
CRYSTAL_LIBRARY_PATH: ${{ github.workspace }}/wasm32-wasi-libs
4646

spec/std/uuid_spec.cr

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,30 @@ describe "UUID" do
162162
expect_raises(ArgumentError) { UUID.new "xyz:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892" }
163163
end
164164

165+
describe "v1" do
166+
it "returns true if UUID is v1, false otherwise" do
167+
uuid = UUID.v1
168+
uuid.v1?.should eq(true)
169+
uuid = UUID.v1(node_id: StaticArray(UInt8, 6).new { |i| (i*10).to_u8 })
170+
uuid.to_s[24..36].should eq("000a141e2832")
171+
end
172+
end
173+
174+
describe "v2" do
175+
it "returns true if UUID is v2, false otherwise" do
176+
uuid = UUID.v2(UUID::Domain::Person, 42)
177+
uuid.v2?.should eq(true)
178+
uuid = UUID.v2(UUID::Domain::Person, 42, node_id: StaticArray(UInt8, 6).new { |i| (i*10).to_u8 })
179+
uuid.to_s[24..36].should eq("000a141e2832")
180+
end
181+
end
182+
165183
describe "v4?" do
166184
it "returns true if UUID is v4, false otherwise" do
167185
uuid = UUID.random
168186
uuid.v4?.should eq(true)
187+
uuid = UUID.v4
188+
uuid.v4?.should eq(true)
169189
uuid = UUID.new("00000000-0000-0000-0000-000000000000", version: UUID::Version::V5)
170190
uuid.v4?.should eq(false)
171191
end
@@ -175,8 +195,78 @@ describe "UUID" do
175195
it "returns true if UUID is v4, raises otherwise" do
176196
uuid = UUID.random
177197
uuid.v4!.should eq(true)
198+
uuid = UUID.v4
199+
uuid.v4!.should eq(true)
178200
uuid = UUID.new("00000000-0000-0000-0000-000000000000", version: UUID::Version::V5)
179201
expect_raises(UUID::Error) { uuid.v4! }
180202
end
181203
end
204+
205+
describe "v3" do
206+
it "generates DNS based names correctly" do
207+
data = "crystal-lang.org"
208+
expected = "60a4b7b5-3333-3f1e-a2cd-30d8a2d0b83b"
209+
UUID.v3(data, UUID::Namespace::DNS).to_s.should eq(expected)
210+
UUID.v3_dns(data).to_s.should eq(expected)
211+
UUID.v3_dns(data).v3?.should eq(true)
212+
end
213+
214+
it "generates URL based names correctly" do
215+
data = "https://crystal-lang.org"
216+
expected = "c25c7b79-5f5f-3844-98a4-2548f5d0e7f9"
217+
UUID.v3(data, UUID::Namespace::URL).to_s.should eq(expected)
218+
UUID.v3_url(data).to_s.should eq(expected)
219+
UUID.v3_url(data).v3?.should eq(true)
220+
end
221+
222+
it "generates OID based names correctly" do
223+
data = "1.3.6.1.4.1.343"
224+
expected = "77bc1dc3-0a9f-3e7e-bfa5-3f611a660c80"
225+
UUID.v3(data, UUID::Namespace::OID).to_s.should eq(expected)
226+
UUID.v3_oid(data).to_s.should eq(expected)
227+
UUID.v3_oid(data).v3?.should eq(true)
228+
end
229+
230+
it "generates X500 based names correctly" do
231+
data = "cn=John Doe, ou=People, o=example, c=com"
232+
expected = "fcab1a4c-fc81-3ebc-9874-9a8b931911d3"
233+
UUID.v3(data, UUID::Namespace::X500).to_s.should eq(expected)
234+
UUID.v3_x500(data).to_s.should eq(expected)
235+
UUID.v3_x500(data).v3?.should eq(true)
236+
end
237+
end
238+
239+
describe "v5" do
240+
it "generates DNS based names correctly" do
241+
data = "crystal-lang.org"
242+
expected = "11caf27c-b803-5e62-9c4b-15332b04047e"
243+
UUID.v5(data, UUID::Namespace::DNS).to_s.should eq(expected)
244+
UUID.v5_dns(data).to_s.should eq(expected)
245+
UUID.v5_dns(data).v5?.should eq(true)
246+
end
247+
248+
it "generates URL based names correctly" do
249+
data = "https://crystal-lang.org"
250+
expected = "29fec3f0-9ad0-5e8a-a42e-214ff695f50e"
251+
UUID.v5(data, UUID::Namespace::URL).to_s.should eq(expected)
252+
UUID.v5_url(data).to_s.should eq(expected)
253+
UUID.v5_url(data).v5?.should eq(true)
254+
end
255+
256+
it "generates OID based names correctly" do
257+
data = "1.3.6.1.4.1.343"
258+
expected = "6aab0456-7392-582a-b92a-ba5a7096945d"
259+
UUID.v5(data, UUID::Namespace::OID).to_s.should eq(expected)
260+
UUID.v5_oid(data).to_s.should eq(expected)
261+
UUID.v5_oid(data).v5?.should eq(true)
262+
end
263+
264+
it "generates X500 based names correctly" do
265+
data = "cn=John Doe, ou=People, o=example, c=com"
266+
expected = "bc10b2d9-f370-5c65-9561-5e3f6d9b236d"
267+
UUID.v5(data, UUID::Namespace::X500).to_s.should eq(expected)
268+
UUID.v5_x500(data).to_s.should eq(expected)
269+
UUID.v5_x500(data).v5?.should eq(true)
270+
end
271+
end
182272
end

src/uuid.cr

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
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+
112
# Represents a UUID (Universally Unique IDentifier).
213
#
314
# NOTE: To use `UUID`, you must explicitly import it with `require "uuid"`
@@ -10,7 +21,7 @@ struct UUID
1021
Unknown
1122
# Reserved by the NCS for backward compatibility.
1223
NCS
13-
# Reserved for RFC4122 Specification (default).
24+
# Reserved for RFC 4122 Specification (default).
1425
RFC4122
1526
# Reserved by Microsoft for backward compatibility.
1627
Microsoft
@@ -22,7 +33,7 @@ struct UUID
2233
enum Version
2334
# Unknown version.
2435
Unknown = 0
25-
# Date-time and MAC address.
36+
# Date-time and NodeID address.
2637
V1 = 1
2738
# DCE security.
2839
V2 = 2
@@ -34,6 +45,31 @@ struct UUID
3445
V5 = 5
3546
end
3647

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+
3773
@bytes : StaticArray(UInt8, 16)
3874

3975
# Generates UUID from *bytes*, applying *version* and *variant* to the UUID if
@@ -176,6 +212,115 @@ struct UUID
176212
new(new_bytes, variant, version)
177213
end
178214

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+
179324
# Generates an empty UUID.
180325
#
181326
# ```

0 commit comments

Comments
 (0)