-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add `UUID` struct. * Support versions and variants. * Remove `uuid` method from `Random` module. * Create UUIDs from `String`, `Slice`, `StaticArray` and other UUIDs
- Loading branch information
Showing
4 changed files
with
331 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
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. | ||
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 |