Skip to content

Commit 1b669c1

Browse files
jkthorneMartin Verzilli
authored and
Martin Verzilli
committedNov 24, 2017
UUID revamp (#4453)
* Add `UUID` struct. * Support versions and variants. * Remove `uuid` method from `Random` module. * Create UUIDs from `String`, `Slice`, `StaticArray` and other UUIDs
1 parent 7a589f7 commit 1b669c1

File tree

4 files changed

+331
-35
lines changed

4 files changed

+331
-35
lines changed
 

‎spec/std/random_spec.cr

-7
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,4 @@ describe "Random" do
260260
hex.should eq("9fd857f462831002ffffffffffffffff0000000000000000e88d3a30db4e730021b8a5e33b020000362f518e0700000062da")
261261
end
262262
end
263-
264-
describe "uuid" do
265-
it "gets uuid" do
266-
uuid = TestRNG.new(RNG_DATA_8).uuid
267-
uuid.should eq("ea990000-7f80-4fff-aa99-00007f80ffff")
268-
end
269-
end
270263
end

‎spec/std/uuid_spec.cr

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
require "spec"
2+
require "uuid"
3+
4+
describe "UUID" do
5+
describe "random initialize" do
6+
it "works with no options" do
7+
subject = UUID.random
8+
subject.variant.should eq UUID::Variant::RFC4122
9+
subject.version.should eq UUID::Version::V4
10+
end
11+
12+
it "works with variant" do
13+
subject = UUID.random(variant: UUID::Variant::NCS)
14+
subject.variant.should eq UUID::Variant::NCS
15+
subject.version.should eq UUID::Version::V4
16+
end
17+
18+
it "works with version" do
19+
subject = UUID.random(version: UUID::Version::V3)
20+
subject.variant.should eq UUID::Variant::RFC4122
21+
subject.version.should eq UUID::Version::V3
22+
end
23+
end
24+
25+
describe "initialize from static array" do
26+
it "works with static array only" do
27+
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8))
28+
subject.to_s.should eq "00000000-0000-0000-0000-000000000000"
29+
end
30+
31+
it "works with static array and variant" do
32+
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), variant: UUID::Variant::RFC4122)
33+
subject.to_s.should eq "00000000-0000-0000-8000-000000000000"
34+
subject.variant.should eq UUID::Variant::RFC4122
35+
end
36+
37+
it "works with static array and version" do
38+
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), version: UUID::Version::V3)
39+
subject.to_s.should eq "00000000-0000-3000-0000-000000000000"
40+
subject.version.should eq UUID::Version::V3
41+
end
42+
43+
it "works with static array, variant and version" do
44+
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), variant: UUID::Variant::Microsoft, version: UUID::Version::V3)
45+
subject.to_s.should eq "00000000-0000-3000-c000-000000000000"
46+
subject.variant.should eq UUID::Variant::Microsoft
47+
subject.version.should eq UUID::Version::V3
48+
end
49+
end
50+
51+
it "initializes with slice" do
52+
subject = UUID.new(Slice(UInt8).new(16, 0_u8), variant: UUID::Variant::RFC4122, version: UUID::Version::V4)
53+
subject.to_s.should eq "00000000-0000-4000-8000-000000000000"
54+
subject.variant.should eq UUID::Variant::RFC4122
55+
subject.version.should eq UUID::Version::V4
56+
end
57+
58+
describe "initialize with String" do
59+
it "works with static array only" do
60+
subject = UUID.new("00000000-0000-0000-0000-000000000000")
61+
subject.to_s.should eq "00000000-0000-0000-0000-000000000000"
62+
end
63+
64+
it "works with static array and variant" do
65+
subject = UUID.new("00000000-0000-0000-0000-000000000000", variant: UUID::Variant::Future)
66+
subject.to_s.should eq "00000000-0000-0000-e000-000000000000"
67+
subject.variant.should eq UUID::Variant::Future
68+
end
69+
70+
it "works with static array and version" do
71+
subject = UUID.new("00000000-0000-0000-0000-000000000000", version: UUID::Version::V5)
72+
subject.to_s.should eq "00000000-0000-5000-0000-000000000000"
73+
subject.version.should eq UUID::Version::V5
74+
end
75+
76+
it "can be built from strings" do
77+
UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
78+
UUID.new("c20335c37f464126aae9f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
79+
UUID.new("C20335C3-7F46-4126-AAE9-F665434AD12B").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
80+
UUID.new("C20335C37F464126AAE9F665434AD12B").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b")
81+
UUID.new("urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").to_s.should eq("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892")
82+
end
83+
end
84+
85+
it "initializes from UUID" do
86+
uuid = UUID.new("50a11da6-377b-4bdf-b9f0-076f9db61c93")
87+
uuid = UUID.new(uuid, version: UUID::Version::V2, variant: UUID::Variant::Microsoft)
88+
uuid.version.should eq UUID::Version::V2
89+
uuid.variant.should eq UUID::Variant::Microsoft
90+
uuid.to_s.should eq "50a11da6-377b-2bdf-d9f0-076f9db61c93"
91+
end
92+
93+
it "initializes zeroed UUID" do
94+
UUID.empty.should eq UUID.new(StaticArray(UInt8, 16).new(0_u8), UUID::Variant::NCS, UUID::Version::V4)
95+
UUID.empty.to_s.should eq "00000000-0000-4000-0000-000000000000"
96+
UUID.empty.variant.should eq UUID::Variant::NCS
97+
UUID.empty.version.should eq UUID::Version::V4
98+
end
99+
100+
describe "supports different string formats" do
101+
it "normal output" do
102+
UUID.new("ee843b2656d8472bb3430b94ed9077ff").to_s.should eq "ee843b26-56d8-472b-b343-0b94ed9077ff"
103+
end
104+
105+
it "hexstring" do
106+
UUID.new("3e806983-eca4-4fc5-b581-f30fb03ec9e5").hexstring.should eq "3e806983eca44fc5b581f30fb03ec9e5"
107+
end
108+
109+
it "urn" do
110+
UUID.new("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").urn.should eq "urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892"
111+
end
112+
end
113+
114+
it "fails on invalid arguments when creating" do
115+
expect_raises(ArgumentError) { UUID.new "25d6f843?cf8e-44fb-9f84-6062419c4330" }
116+
expect_raises(ArgumentError) { UUID.new "67dc9e24-0865 474b-9fe7-61445bfea3b5" }
117+
expect_raises(ArgumentError) { UUID.new "5942cde5-10d1-416b+85c4-9fc473fa1037" }
118+
expect_raises(ArgumentError) { UUID.new "0f02a229-4898-4029-926f=94be5628a7fd" }
119+
expect_raises(ArgumentError) { UUID.new "cda08c86-6413-474f-8822-a6646e0fb19G" }
120+
expect_raises(ArgumentError) { UUID.new "2b1bfW06368947e59ac07c3ffdaf514c" }
121+
end
122+
end

‎src/random.cr

-28
Original file line numberDiff line numberDiff line change
@@ -373,34 +373,6 @@ module Random
373373
random_bytes(n).hexstring
374374
end
375375

376-
# Generates a UUID (Universally Unique Identifier).
377-
#
378-
# It generates a random v4 UUID. See
379-
# [RFC 4122 Section 4.4](https://tools.ietf.org/html/rfc4122#section-4.4)
380-
# for the used algorithm and its implications.
381-
#
382-
# ```
383-
# Random::Secure.uuid # => "a4e319dd-a778-4a51-804e-66a07bc63358"
384-
# ```
385-
#
386-
# It is recommended to use the secure `Random::Secure` as a source or another
387-
# cryptographically quality PRNG such as `Random::ISAAC` or ChaCha20.
388-
def uuid : String
389-
bytes = random_bytes(16)
390-
bytes[6] = (bytes[6] & 0x0f) | 0x40
391-
bytes[8] = (bytes[8] & 0x3f) | 0x80
392-
393-
String.new(36) do |buffer|
394-
buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8
395-
bytes[0, 4].hexstring(buffer + 0)
396-
bytes[4, 2].hexstring(buffer + 9)
397-
bytes[6, 2].hexstring(buffer + 14)
398-
bytes[8, 2].hexstring(buffer + 19)
399-
bytes[10, 6].hexstring(buffer + 24)
400-
{36, 36}
401-
end
402-
end
403-
404376
# See `#rand`.
405377
def self.rand : Float64
406378
DEFAULT.rand

‎src/uuid.cr

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Represents a UUID (Universally Unique IDentifier).
2+
struct UUID
3+
enum Variant # variants with 16 bytes.
4+
Unknown # Unknown (ie. custom, your own).
5+
NCS # Reserved by the NCS for backward compatibility.
6+
RFC4122 # Reserved for RFC4122 Specification (default).
7+
Microsoft # Reserved by Microsoft for backward compatibility.
8+
Future # Reserved for future expansion.
9+
end
10+
11+
enum Version # RFC4122 UUID versions.
12+
Unknown = 0 # Unknown version.
13+
V1 = 1 # date-time and MAC address.
14+
V2 = 2 # DCE security.
15+
V3 = 3 # MD5 hash and namespace.
16+
V4 = 4 # random.
17+
V5 = 5 # SHA1 hash and namespace.
18+
end
19+
20+
protected getter bytes : StaticArray(UInt8, 16)
21+
22+
# Generates UUID from *bytes*, applying *version* and *variant* to the UUID if
23+
# present.
24+
def initialize(@bytes : StaticArray(UInt8, 16), variant : UUID::Variant? = nil, version : UUID::Version? = nil)
25+
case variant
26+
when nil
27+
# do nothing
28+
when Variant::NCS
29+
@bytes[8] = (@bytes[8] & 0x7f)
30+
when Variant::RFC4122
31+
@bytes[8] = (@bytes[8] & 0x3f) | 0x80
32+
when Variant::Microsoft
33+
@bytes[8] = (@bytes[8] & 0x1f) | 0xc0
34+
when Variant::Future
35+
@bytes[8] = (@bytes[8] & 0x1f) | 0xe0
36+
else
37+
raise ArgumentError.new "Can't set unknown variant"
38+
end
39+
40+
if version
41+
raise ArgumentError.new "Can't set unknown version" if version.unknown?
42+
@bytes[6] = (@bytes[6] & 0xf) | (version.to_u8 << 4)
43+
end
44+
end
45+
46+
# Creates UUID from 16-bytes slice. Raises if *slice* isn't 16 bytes long. See
47+
# `#initialize` for *variant* and *version*.
48+
def self.new(slice : Slice(UInt8), variant = nil, version = nil)
49+
raise ArgumentError.new "Invalid bytes length #{slice.size}, expected 16" unless slice.size == 16
50+
51+
bytes = uninitialized UInt8[16]
52+
slice.copy_to(bytes.to_slice)
53+
54+
new(bytes, variant, version)
55+
end
56+
57+
# Creates another `UUID` which is a copy of *uuid*, but allows overriding
58+
# *variant* or *version*.
59+
def self.new(uuid : UUID, variant = nil, version = nil)
60+
new(uuid.bytes, variant, version)
61+
end
62+
63+
# Creates new UUID by decoding `value` string from hyphenated (ie. `ba714f86-cac6-42c7-8956-bcf5105e1b81`),
64+
# hexstring (ie. `89370a4ab66440c8add39e06f2bb6af6`) or URN (ie. `urn:uuid:3f9eaf9e-cdb0-45cc-8ecb-0e5b2bfb0c20`)
65+
# format.
66+
def self.new(value : String, variant = nil, version = nil)
67+
bytes = uninitialized UInt8[16]
68+
69+
case value.size
70+
when 36 # Hyphenated
71+
[8, 13, 18, 23].each do |offset|
72+
if value[offset] != '-'
73+
raise ArgumentError.new "Invalid UUID string format, expected hyphen at char #{offset}"
74+
end
75+
end
76+
[0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34].each_with_index do |offset, i|
77+
string_has_hex_pair_at! value, offset
78+
bytes[i] = value[offset, 2].to_u8(16)
79+
end
80+
when 32 # Hexstring
81+
16.times do |i|
82+
string_has_hex_pair_at! value, i * 2
83+
bytes[i] = value[i * 2, 2].to_u8(16)
84+
end
85+
when 45 # URN
86+
raise ArgumentError.new "Invalid URN UUID format, expected string starting with \":urn:uuid:\"" unless value.starts_with? "urn:uuid:"
87+
[9, 11, 13, 15, 18, 20, 23, 25, 28, 30, 33, 35, 37, 39, 41, 43].each_with_index do |offset, i|
88+
string_has_hex_pair_at! value, offset
89+
bytes[i] = value[offset, 2].to_u8(16)
90+
end
91+
else
92+
raise ArgumentError.new "Invalid string length #{value.size} for UUID, expected 32 (hexstring), 36 (hyphenated) or 46 (urn)"
93+
end
94+
95+
new(bytes, variant, version)
96+
end
97+
98+
# Raises `ArgumentError` if string `value` at index `i` doesn't contain hex
99+
# digit followed by another hex digit.
100+
private def self.string_has_hex_pair_at!(value : String, i)
101+
unless value[i, 2].to_u8(16, whitespace: false, underscore: false, prefix: false)
102+
raise ArgumentError.new [
103+
"Invalid hex character at position #{i * 2} or #{i * 2 + 1}",
104+
"expected '0' to '9', 'a' to 'f' or 'A' to 'F'",
105+
].join(", ")
106+
end
107+
end
108+
109+
# Generates RFC 4122 v4 UUID.
110+
#
111+
# It is strongly recommended to use a cryptographically random source for
112+
# *random*, such as `Random::Secure`.
113+
def self.random(random = Random::Secure, variant = Variant::RFC4122, version = Version::V4)
114+
new_bytes = uninitialized UInt8[16]
115+
random.random_bytes(new_bytes.to_slice)
116+
117+
new(new_bytes, variant, version)
118+
end
119+
120+
def self.empty
121+
new(StaticArray(UInt8, 16).new(0_u8), UUID::Variant::NCS, UUID::Version::V4)
122+
end
123+
124+
# Returns UUID variant.
125+
def variant
126+
case
127+
when @bytes[8] & 0x80 == 0x00
128+
Variant::NCS
129+
when @bytes[8] & 0xc0 == 0x80
130+
Variant::RFC4122
131+
when @bytes[8] & 0xe0 == 0xc0
132+
Variant::Microsoft
133+
when @bytes[8] & 0xe0 == 0xe0
134+
Variant::Future
135+
else
136+
Variant::Unknown
137+
end
138+
end
139+
140+
# Returns version based on RFC4122 format. See also `#variant`.
141+
def version
142+
case @bytes[6] >> 4
143+
when 1 then Version::V1
144+
when 2 then Version::V2
145+
when 3 then Version::V3
146+
when 4 then Version::V4
147+
when 5 then Version::V5
148+
else Version::Unknown
149+
end
150+
end
151+
152+
# Returns 16-byte slice.
153+
def to_slice
154+
@bytes.to_slice
155+
end
156+
157+
# Returns unsafe pointer to 16-bytes.
158+
def to_unsafe
159+
@bytes.to_unsafe
160+
end
161+
162+
# Returns `true` if `other` UUID represents the same UUID, `false` otherwise.
163+
def ==(other : UUID)
164+
to_slice == other.to_slice
165+
end
166+
167+
def to_s(io : IO)
168+
slice = to_slice
169+
170+
buffer = uninitialized UInt8[36]
171+
buffer_ptr = buffer.to_unsafe
172+
173+
buffer_ptr[8] = buffer_ptr[13] = buffer_ptr[18] = buffer_ptr[23] = '-'.ord.to_u8
174+
slice[0, 4].hexstring(buffer_ptr + 0)
175+
slice[4, 2].hexstring(buffer_ptr + 9)
176+
slice[6, 2].hexstring(buffer_ptr + 14)
177+
slice[8, 2].hexstring(buffer_ptr + 19)
178+
slice[10, 6].hexstring(buffer_ptr + 24)
179+
180+
io.write(buffer.to_slice)
181+
end
182+
183+
def hexstring
184+
to_slice.hexstring
185+
end
186+
187+
def urn
188+
String.build(45) do |str|
189+
str << "urn:uuid:"
190+
to_s(str)
191+
end
192+
end
193+
194+
{% for v in %w(1 2 3 4 5) %}
195+
# Returns `true` if UUID looks is a V{{ v.id }}, `false` otherwise.
196+
def v{{ v.id }}?
197+
variant == Variant::RFC4122 && version == RFC4122::Version::V{{ v.id }}
198+
end
199+
200+
# Returns `true` if UUID looks is a V{{ v.id }}, raises `Error` otherwise.
201+
def v{{ v.id }}!
202+
unless v{{ v.id }}?
203+
raise Error.new("Invalid UUID variant #{variant} version #{version}, expected RFC 4122 V{{ v.id }}")
204+
else
205+
true
206+
end
207+
end
208+
{% end %}
209+
end

0 commit comments

Comments
 (0)
Please sign in to comment.