-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UUID continuation #4453
UUID continuation #4453
Changes from all commits
3b1bae7
f0bb4d8
a9019b6
cbfd749
a2e23a3
3d69d69
133e84d
c6a9604
0ca59d6
2d881d9
e48a7d5
a48e4fb
64e64f7
df3fd11
55ad17a
c1cf14d
8295cfa
b7c17f3
c6b781c
8fee1b6
f72b57b
7139655
f3fce3d
dfbc0c1
a71f569
41302ca
0f7d36f
9242f73
baa9277
e34308e
5dce117
ad9a37f
509ff75
e55718d
4cfd90e
79f5b7d
7b4b9ad
2cae97a
f369a34
b325f3d
ac2d176
9290bf7
6044754
627a8e1
cc14710
8ff542e
8639668
8d25980
8062232
8f47d38
d79b51f
9828bd7
0366f78
409fe5b
e3bee29
49a6372
17ddbbf
f282061
a80ff09
b4f1340
4669f9a
1c3e811
3addefc
30db4ad
eec5ece
494d20b
6a25156
264f729
6a143fa
1c8db2b
3712820
f6f6070
122f84b
c496177
9c49e7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
require "spec" | ||
require "uuid" | ||
|
||
describe "UUID" do | ||
describe "random initialize" do | ||
it "works with no options" do | ||
subject = UUID.random | ||
subject.variant.should eq UUID::Variant::RFC4122 | ||
subject.version.should eq UUID::Version::V4 | ||
end | ||
|
||
it "works with variant" do | ||
subject = UUID.random(variant: UUID::Variant::NCS) | ||
subject.variant.should eq UUID::Variant::NCS | ||
subject.version.should eq UUID::Version::V4 | ||
end | ||
|
||
it "works with version" do | ||
subject = UUID.random(version: UUID::Version::V3) | ||
subject.variant.should eq UUID::Variant::RFC4122 | ||
subject.version.should eq UUID::Version::V3 | ||
end | ||
end | ||
|
||
describe "initialize from static array" do | ||
it "works with static array only" do | ||
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8)) | ||
subject.to_s.should eq "00000000-0000-0000-0000-000000000000" | ||
end | ||
|
||
it "works with static array and variant" do | ||
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), variant: UUID::Variant::RFC4122) | ||
subject.to_s.should eq "00000000-0000-0000-8000-000000000000" | ||
subject.variant.should eq UUID::Variant::RFC4122 | ||
end | ||
|
||
it "works with static array and version" do | ||
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), version: UUID::Version::V3) | ||
subject.to_s.should eq "00000000-0000-3000-0000-000000000000" | ||
subject.version.should eq UUID::Version::V3 | ||
end | ||
|
||
it "works with static array, variant and version" do | ||
subject = UUID.new(StaticArray(UInt8, 16).new(0_u8), variant: UUID::Variant::Microsoft, version: UUID::Version::V3) | ||
subject.to_s.should eq "00000000-0000-3000-c000-000000000000" | ||
subject.variant.should eq UUID::Variant::Microsoft | ||
subject.version.should eq UUID::Version::V3 | ||
end | ||
end | ||
|
||
it "initializes with slice" do | ||
subject = UUID.new(Slice(UInt8).new(16, 0_u8), variant: UUID::Variant::RFC4122, version: UUID::Version::V4) | ||
subject.to_s.should eq "00000000-0000-4000-8000-000000000000" | ||
subject.variant.should eq UUID::Variant::RFC4122 | ||
subject.version.should eq UUID::Version::V4 | ||
end | ||
|
||
describe "initialize with String" do | ||
it "works with static array only" do | ||
subject = UUID.new("00000000-0000-0000-0000-000000000000") | ||
subject.to_s.should eq "00000000-0000-0000-0000-000000000000" | ||
end | ||
|
||
it "works with static array and variant" do | ||
subject = UUID.new("00000000-0000-0000-0000-000000000000", variant: UUID::Variant::Future) | ||
subject.to_s.should eq "00000000-0000-0000-e000-000000000000" | ||
subject.variant.should eq UUID::Variant::Future | ||
end | ||
|
||
it "works with static array and version" do | ||
subject = UUID.new("00000000-0000-0000-0000-000000000000", version: UUID::Version::V5) | ||
subject.to_s.should eq "00000000-0000-5000-0000-000000000000" | ||
subject.version.should eq UUID::Version::V5 | ||
end | ||
|
||
it "can be built from strings" do | ||
UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
UUID.new("c20335c37f464126aae9f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
UUID.new("C20335C3-7F46-4126-AAE9-F665434AD12B").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
UUID.new("C20335C37F464126AAE9F665434AD12B").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b") | ||
UUID.new("urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").to_s.should eq("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892") | ||
end | ||
end | ||
|
||
it "initializes from UUID" do | ||
uuid = UUID.new("50a11da6-377b-4bdf-b9f0-076f9db61c93") | ||
uuid = UUID.new(uuid, version: UUID::Version::V2, variant: UUID::Variant::Microsoft) | ||
uuid.version.should eq UUID::Version::V2 | ||
uuid.variant.should eq UUID::Variant::Microsoft | ||
uuid.to_s.should eq "50a11da6-377b-2bdf-d9f0-076f9db61c93" | ||
end | ||
|
||
it "initializes zeroed UUID" do | ||
UUID.empty.should eq UUID.new(StaticArray(UInt8, 16).new(0_u8), UUID::Variant::NCS, UUID::Version::V4) | ||
UUID.empty.to_s.should eq "00000000-0000-4000-0000-000000000000" | ||
UUID.empty.variant.should eq UUID::Variant::NCS | ||
UUID.empty.version.should eq UUID::Version::V4 | ||
end | ||
|
||
describe "supports different string formats" do | ||
it "normal output" do | ||
UUID.new("ee843b2656d8472bb3430b94ed9077ff").to_s.should eq "ee843b26-56d8-472b-b343-0b94ed9077ff" | ||
end | ||
|
||
it "hexstring" do | ||
UUID.new("3e806983-eca4-4fc5-b581-f30fb03ec9e5").hexstring.should eq "3e806983eca44fc5b581f30fb03ec9e5" | ||
end | ||
|
||
it "urn" do | ||
UUID.new("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").urn.should eq "urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892" | ||
end | ||
end | ||
|
||
it "fails on invalid arguments when creating" do | ||
expect_raises(ArgumentError) { UUID.new "25d6f843?cf8e-44fb-9f84-6062419c4330" } | ||
expect_raises(ArgumentError) { UUID.new "67dc9e24-0865 474b-9fe7-61445bfea3b5" } | ||
expect_raises(ArgumentError) { UUID.new "5942cde5-10d1-416b+85c4-9fc473fa1037" } | ||
expect_raises(ArgumentError) { UUID.new "0f02a229-4898-4029-926f=94be5628a7fd" } | ||
expect_raises(ArgumentError) { UUID.new "cda08c86-6413-474f-8822-a6646e0fb19G" } | ||
expect_raises(ArgumentError) { UUID.new "2b1bfW06368947e59ac07c3ffdaf514c" } | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
# Represents a UUID (Universally Unique IDentifier). | ||
struct UUID | ||
enum Variant # variants with 16 bytes. | ||
Unknown # Unknown (ie. custom, your own). | ||
NCS # Reserved by the NCS for backward compatibility. | ||
RFC4122 # Reserved for RFC4122 Specification (default). | ||
Microsoft # Reserved by Microsoft for backward compatibility. | ||
Future # Reserved for future expansion. | ||
end | ||
|
||
enum Version # RFC4122 UUID versions. | ||
Unknown = 0 # Unknown version. | ||
V1 = 1 # date-time and MAC address. | ||
V2 = 2 # DCE security. | ||
V3 = 3 # MD5 hash and namespace. | ||
V4 = 4 # random. | ||
V5 = 5 # SHA1 hash and namespace. | ||
end | ||
|
||
protected getter bytes : StaticArray(UInt8, 16) | ||
|
||
# Generates UUID from *bytes*, applying *version* and *variant* to the UUID if | ||
# present. | ||
def initialize(@bytes : StaticArray(UInt8, 16), variant : UUID::Variant? = nil, version : UUID::Version? = nil) | ||
case variant | ||
when nil | ||
# do nothing | ||
when Variant::NCS | ||
@bytes[8] = (@bytes[8] & 0x7f) | ||
when Variant::RFC4122 | ||
@bytes[8] = (@bytes[8] & 0x3f) | 0x80 | ||
when Variant::Microsoft | ||
@bytes[8] = (@bytes[8] & 0x1f) | 0xc0 | ||
when Variant::Future | ||
@bytes[8] = (@bytes[8] & 0x1f) | 0xe0 | ||
else | ||
raise ArgumentError.new "Can't set unknown variant" | ||
end | ||
|
||
if version | ||
raise ArgumentError.new "Can't set unknown version" if version.unknown? | ||
@bytes[6] = (@bytes[6] & 0xf) | (version.to_u8 << 4) | ||
end | ||
end | ||
|
||
# Creates UUID from 16-bytes slice. Raises if *slice* isn't 16 bytes long. See | ||
# `#initialize` for *variant* and *version*. | ||
def self.new(slice : Slice(UInt8), variant = nil, version = nil) | ||
raise ArgumentError.new "Invalid bytes length #{slice.size}, expected 16" unless slice.size == 16 | ||
|
||
bytes = uninitialized UInt8[16] | ||
slice.copy_to(bytes.to_slice) | ||
|
||
new(bytes, variant, version) | ||
end | ||
|
||
# Creates another `UUID` which is a copy of *uuid*, but allows overriding | ||
# *variant* or *version*. | ||
def self.new(uuid : UUID, variant = nil, version = nil) | ||
new(uuid.bytes, variant, version) | ||
end | ||
|
||
# Creates new UUID by decoding `value` string from hyphenated (ie. `ba714f86-cac6-42c7-8956-bcf5105e1b81`), | ||
# hexstring (ie. `89370a4ab66440c8add39e06f2bb6af6`) or URN (ie. `urn:uuid:3f9eaf9e-cdb0-45cc-8ecb-0e5b2bfb0c20`) | ||
# format. | ||
def self.new(value : String, variant = nil, version = nil) | ||
bytes = uninitialized UInt8[16] | ||
|
||
case value.size | ||
when 36 # Hyphenated | ||
[8, 13, 18, 23].each do |offset| | ||
if value[offset] != '-' | ||
raise ArgumentError.new "Invalid UUID string format, expected hyphen at char #{offset}" | ||
end | ||
end | ||
[0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34].each_with_index do |offset, i| | ||
string_has_hex_pair_at! value, offset | ||
bytes[i] = value[offset, 2].to_u8(16) | ||
end | ||
when 32 # Hexstring | ||
16.times do |i| | ||
string_has_hex_pair_at! value, i * 2 | ||
bytes[i] = value[i * 2, 2].to_u8(16) | ||
end | ||
when 45 # URN | ||
raise ArgumentError.new "Invalid URN UUID format, expected string starting with \":urn:uuid:\"" unless value.starts_with? "urn:uuid:" | ||
[9, 11, 13, 15, 18, 20, 23, 25, 28, 30, 33, 35, 37, 39, 41, 43].each_with_index do |offset, i| | ||
string_has_hex_pair_at! value, offset | ||
bytes[i] = value[offset, 2].to_u8(16) | ||
end | ||
else | ||
raise ArgumentError.new "Invalid string length #{value.size} for UUID, expected 32 (hexstring), 36 (hyphenated) or 46 (urn)" | ||
end | ||
|
||
new(bytes, variant, version) | ||
end | ||
|
||
# Raises `ArgumentError` if string `value` at index `i` doesn't contain hex | ||
# digit followed by another hex digit. | ||
private def self.string_has_hex_pair_at!(value : String, i) | ||
unless value[i, 2].to_u8(16, whitespace: false, underscore: false, prefix: false) | ||
raise ArgumentError.new [ | ||
"Invalid hex character at position #{i * 2} or #{i * 2 + 1}", | ||
"expected '0' to '9', 'a' to 'f' or 'A' to 'F'", | ||
].join(", ") | ||
end | ||
end | ||
|
||
# Generates RFC 4122 v4 UUID. | ||
# | ||
# It is strongly recommended to use a cryptographically random source for | ||
# *random*, such as `Random::Secure`. | ||
def self.random(random = Random::Secure, variant = Variant::RFC4122, version = Version::V4) | ||
new_bytes = uninitialized UInt8[16] | ||
random.random_bytes(new_bytes.to_slice) | ||
|
||
new(new_bytes, variant, version) | ||
end | ||
|
||
def self.empty | ||
new(StaticArray(UInt8, 16).new(0_u8), UUID::Variant::NCS, UUID::Version::V4) | ||
end | ||
|
||
# Returns UUID variant. | ||
def variant | ||
case | ||
when @bytes[8] & 0x80 == 0x00 | ||
Variant::NCS | ||
when @bytes[8] & 0xc0 == 0x80 | ||
Variant::RFC4122 | ||
when @bytes[8] & 0xe0 == 0xc0 | ||
Variant::Microsoft | ||
when @bytes[8] & 0xe0 == 0xe0 | ||
Variant::Future | ||
else | ||
Variant::Unknown | ||
end | ||
end | ||
|
||
# Returns version based on RFC4122 format. See also `#variant`. | ||
def version | ||
case @bytes[6] >> 4 | ||
when 1 then Version::V1 | ||
when 2 then Version::V2 | ||
when 3 then Version::V3 | ||
when 4 then Version::V4 | ||
when 5 then Version::V5 | ||
else Version::Unknown | ||
end | ||
end | ||
|
||
# Returns 16-byte slice. | ||
def to_slice | ||
@bytes.to_slice | ||
end | ||
|
||
# Returns unsafe pointer to 16-bytes. | ||
def to_unsafe | ||
@bytes.to_unsafe | ||
end | ||
|
||
# Returns `true` if `other` UUID represents the same UUID, `false` otherwise. | ||
def ==(other : UUID) | ||
to_slice == other.to_slice | ||
end | ||
|
||
def to_s(io : IO) | ||
slice = to_slice | ||
|
||
buffer = uninitialized UInt8[36] | ||
buffer_ptr = buffer.to_unsafe | ||
|
||
buffer_ptr[8] = buffer_ptr[13] = buffer_ptr[18] = buffer_ptr[23] = '-'.ord.to_u8 | ||
slice[0, 4].hexstring(buffer_ptr + 0) | ||
slice[4, 2].hexstring(buffer_ptr + 9) | ||
slice[6, 2].hexstring(buffer_ptr + 14) | ||
slice[8, 2].hexstring(buffer_ptr + 19) | ||
slice[10, 6].hexstring(buffer_ptr + 24) | ||
|
||
io.write(buffer.to_slice) | ||
end | ||
|
||
def hexstring | ||
to_slice.hexstring | ||
end | ||
|
||
def urn | ||
String.build(45) do |str| | ||
str << "urn:uuid:" | ||
to_s(str) | ||
end | ||
end | ||
|
||
{% for v in %w(1 2 3 4 5) %} | ||
# Returns `true` if UUID looks is a V{{ v.id }}, `false` otherwise. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean "if UUID looks as"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "If UUID has version Vx" would make more sense surely? |
||
def v{{ v.id }}? | ||
variant == Variant::RFC4122 && version == RFC4122::Version::V{{ v.id }} | ||
end | ||
|
||
# Returns `true` if UUID looks is a V{{ v.id }}, raises `Error` otherwise. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||
def v{{ v.id }}! | ||
unless v{{ v.id }}? | ||
raise Error.new("Invalid UUID variant #{variant} version #{version}, expected RFC 4122 V{{ v.id }}") | ||
else | ||
true | ||
end | ||
end | ||
{% end %} | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why there's no replacement of
Random#uuid
?It could boil down to:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...because
UUID.new
doesn't use the random instance you're calling#uuid
on, it always usesRandom::Secure
. And for good reason.