From 3b1bae7bd0a205896c9bec2ac02c7a3fefcb1283 Mon Sep 17 00:00:00 2001 From: Mirek Rusin Date: Wed, 1 Jun 2016 20:46:51 -0300 Subject: [PATCH 1/5] Add `UUID`. --- spec/std/uuid_spec.cr | 38 +++++++++++++++ src/prelude.cr | 1 + src/uuid.cr | 106 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 spec/std/uuid_spec.cr create mode 100644 src/uuid.cr diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr new file mode 100644 index 000000000000..535b13da2aae --- /dev/null +++ b/spec/std/uuid_spec.cr @@ -0,0 +1,38 @@ +require "spec" + +describe "UUID" do + + it "can be built from strings" do + UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") + UUID.new("c20335c37f464126aae9f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") + UUID.new("C20335C3-7F46-4126-AAE9-F665434AD12B").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") + UUID.new("C20335C37F464126AAE9F665434AD12B").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") + end + + it "compares to strings" do + uuid = UUID.new "c3b46146eb794e18877b4d46a10d1517" + ->{ uuid == "c3b46146eb794e18877b4d46a10d1517" }.call.should eq(true) + ->{ uuid == "c3b46146-eb79-4e18-877b-4d46a10d1517" }.call.should eq(true) + ->{ uuid == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(true) + ->{ UUID.new == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(false) + end + + it "fails on invalid arguments when creating" do + expect_raises(ArgumentError) { UUID.new "" } + 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 + + it "fails when comparing to invalid strings" do + expect_raises(ArgumentError) { UUID.new == "" } + expect_raises(ArgumentError) { UUID.new == "d1fb9189-7013-4915-a8b1-07cfc83bca3U" } + expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e0 6c" } + expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e 06c" } + expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e-76c" } + end + +end diff --git a/src/prelude.cr b/src/prelude.cr index a797ec392e76..88078350b718 100644 --- a/src/prelude.cr +++ b/src/prelude.cr @@ -70,4 +70,5 @@ require "thread" require "time" require "tuple" require "union" +require "uuid" require "value" diff --git a/src/uuid.cr b/src/uuid.cr new file mode 100644 index 000000000000..f0d95fd47e9c --- /dev/null +++ b/src/uuid.cr @@ -0,0 +1,106 @@ + +require "secure_random" + +private def assert_hex_pair_at!(value : String, i) + unless value[i].hex? && value[i + 1].hex? + 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 + +# Universally Unique Identifier. +struct UUID + + enum Version + Unknown + V1 + V2 + V3 + V4 + V5 + end + + @data = StaticArray(UInt8, 16).new + + def initialize(bytes : Slice(UInt8)) + raise ArgumentError.new "Invalid bytes length #{bytes.size}, expected 16." if bytes.size != 16 + @data.to_unsafe.copy_from bytes + end + + def initialize(version = Version::V4) + case version + when Version::V4 + @data.to_unsafe.copy_from SecureRandom.random_bytes(16).to_unsafe, 16 + @data[6] = (@data[6] & 0x0f) | 0x40 + @data[8] = (@data[8] & 0x3f) | 0x80 + else + raise ArgumentError.new "Unsupported version #{version}." + end + end + + def initialize(value : String) + case value.size + when 36 # with hyphens + [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| + assert_hex_pair_at! value, offset + @data[i] = value[offset, 2].to_u8(16) + end + when 32 # without hyphens + 16.times do |i| + assert_hex_pair_at! value, i * 2 + @data[i] = value[i * 2, 2].to_u8(16) + end + else + raise ArgumentError.new "Invalid string length #{value.size} for UUID, expected 32 (hex) or 36 (hyphenated hex)." + end + end + + def to_slice + Slice(UInt8).new to_unsafe, 16 + end + + def to_unsafe + @data.to_unsafe + end + + def to_s(io : IO) + io << to_s(true) + end + + def to_s(hyphenated = true) + slice = to_slice + if hyphenated + String.new(36) do |buffer| + buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8 + slice[0, 4].hexstring(buffer + 0) + slice[4, 2].hexstring(buffer + 9) + slice[6, 2].hexstring(buffer + 14) + slice[8, 2].hexstring(buffer + 19) + slice[10, 6].hexstring(buffer + 24) + {36, 36} + end + else + slice.hexstring + end + end + + def ==(other : String) + self == UUID.new other + end + + def ==(other : Slice(UInt8)) + to_slice == other + end + + def ==(other : StaticArray(UInt8, 16)) + self.==(Slice(UInt8).new other.to_unsafe, 16) + end + +end From f0bb4d8390e1153577bbf6fb85698de3041df5c4 Mon Sep 17 00:00:00 2001 From: Mirek Rusin Date: Thu, 2 Jun 2016 13:02:15 -0300 Subject: [PATCH 2/5] Rearrange structure to separate RFC4122 logic. --- spec/std/uuid_spec.cr | 2 - src/uuid.cr | 117 ++++++------------------------------------ src/uuid/rfc4122.cr | 39 ++++++++++++++ src/uuid/uuid.cr | 90 ++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 102 deletions(-) create mode 100644 src/uuid/rfc4122.cr create mode 100644 src/uuid/uuid.cr diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr index 535b13da2aae..6cf3771b68b8 100644 --- a/spec/std/uuid_spec.cr +++ b/spec/std/uuid_spec.cr @@ -1,7 +1,6 @@ require "spec" describe "UUID" do - it "can be built from strings" do UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") UUID.new("c20335c37f464126aae9f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") @@ -34,5 +33,4 @@ describe "UUID" do expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e 06c" } expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e-76c" } end - end diff --git a/src/uuid.cr b/src/uuid.cr index f0d95fd47e9c..b2ca2cd3bbe1 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -1,106 +1,23 @@ - require "secure_random" - -private def assert_hex_pair_at!(value : String, i) - unless value[i].hex? && value[i + 1].hex? - 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 - -# Universally Unique Identifier. -struct UUID - - enum Version - Unknown - V1 - V2 - V3 - V4 - V5 - end - - @data = StaticArray(UInt8, 16).new - - def initialize(bytes : Slice(UInt8)) - raise ArgumentError.new "Invalid bytes length #{bytes.size}, expected 16." if bytes.size != 16 - @data.to_unsafe.copy_from bytes - end - - def initialize(version = Version::V4) - case version - when Version::V4 - @data.to_unsafe.copy_from SecureRandom.random_bytes(16).to_unsafe, 16 - @data[6] = (@data[6] & 0x0f) | 0x40 - @data[8] = (@data[8] & 0x3f) | 0x80 - else - raise ArgumentError.new "Unsupported version #{version}." - end - end - - def initialize(value : String) - case value.size - when 36 # with hyphens - [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| - assert_hex_pair_at! value, offset - @data[i] = value[offset, 2].to_u8(16) - end - when 32 # without hyphens - 16.times do |i| - assert_hex_pair_at! value, i * 2 - @data[i] = value[i * 2, 2].to_u8(16) - end - else - raise ArgumentError.new "Invalid string length #{value.size} for UUID, expected 32 (hex) or 36 (hyphenated hex)." +require "./uuid/*" + +# Universally Unique IDentifier. +# +# Supports custom variants with arbitrary 16 bytes as well as (RFC 4122)[https://www.ietf.org/rfc/rfc4122.txt] variant +# versions. +module UUID + # Raises `ArgumentError` if string `value` at index `i` doesn't contain hex digit followed by another hex digit. + # TODO: Move to String#digit?(...) + def self.string_has_hex_pair_at!(value : String, i) + unless value[i].hex? && value[i + 1].hex? + 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 - def to_slice - Slice(UInt8).new to_unsafe, 16 + def self.new(*args) + ::UUID::UUID.new *args end - - def to_unsafe - @data.to_unsafe - end - - def to_s(io : IO) - io << to_s(true) - end - - def to_s(hyphenated = true) - slice = to_slice - if hyphenated - String.new(36) do |buffer| - buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8 - slice[0, 4].hexstring(buffer + 0) - slice[4, 2].hexstring(buffer + 9) - slice[6, 2].hexstring(buffer + 14) - slice[8, 2].hexstring(buffer + 19) - slice[10, 6].hexstring(buffer + 24) - {36, 36} - end - else - slice.hexstring - end - end - - def ==(other : String) - self == UUID.new other - end - - def ==(other : Slice(UInt8)) - to_slice == other - end - - def ==(other : StaticArray(UInt8, 16)) - self.==(Slice(UInt8).new other.to_unsafe, 16) - end - end diff --git a/src/uuid/rfc4122.cr b/src/uuid/rfc4122.cr new file mode 100644 index 000000000000..ecea8bb845fe --- /dev/null +++ b/src/uuid/rfc4122.cr @@ -0,0 +1,39 @@ +# Support for RFC 4122 UUID variant. +module UUID::RFC4122 + enum RFC4122Version + Unknown + V1 + V2 + V3 + V4 + V5 + end + + # Generates RFC UUID variant with specified format `version`. + def initialize(version : RFC4122Version) + case version + when RFC4122Version::V4 + @data.to_unsafe.copy_from SecureRandom.random_bytes(16).to_unsafe, 16 + @data[6] = (@data[6] & 0x0f) | 0x40 + @data[8] = (@data[8] & 0x3f) | 0x80 + else + raise ArgumentError.new "Unsupported version #{version}." + end + end + + {% for version in %w(1 2 3 4 5) %} + + def v{{ version.id }}? + rfc4122_version == RFC4122Version::V{{ version.id }} + end + + def v{{ version.id }}! + unless v{{ version.id }}? + raise Error.new("Invalid RFC 4122 UUID version #{rfc_4122}, expected V{{ version.id }}.") + else + true + end + end + + {% end %} +end diff --git a/src/uuid/uuid.cr b/src/uuid/uuid.cr new file mode 100644 index 000000000000..548317b29d03 --- /dev/null +++ b/src/uuid/uuid.cr @@ -0,0 +1,90 @@ +module UUID + struct UUID + include ::UUID::RFC4122 + + # Internal representation. + @data = StaticArray(UInt8, 16).new + + # Generates UUID (RFC 4122 v4). + def initialize + initialize RFC4122Version::V4 + end + + # Creates UUID from any 16 `bytes` slice. + def initialize(bytes : Slice(UInt8)) + raise ArgumentError.new "Invalid bytes length #{bytes.size}, expected 16." if bytes.size != 16 + @data.to_unsafe.copy_from bytes + end + + # Creates UUID from (optionally hyphenated) string `value`. + def initialize(value : String) + case value.size + when 36 # with hyphens + [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| + ::UUID.string_has_hex_pair_at! value, offset + @data[i] = value[offset, 2].to_u8(16) + end + when 32 # without hyphens + 16.times do |i| + ::UUID.string_has_hex_pair_at! value, i * 2 + @data[i] = value[i * 2, 2].to_u8(16) + end + else + raise ArgumentError.new "Invalid string length #{value.size} for UUID, expected 32 (hex) or 36 (hyphenated hex)." + end + end + + # Returns 16-byte slice of this UUID. + def to_slice + Slice(UInt8).new to_unsafe, 16 + end + + # Returns unsafe pointer to 16-byte slice of this UUID. + def to_unsafe + @data.to_unsafe + end + + # Writes hyphenated string representation for this UUID. + def to_s(io : IO) + io << to_s(true) + end + + # Returns (optionally `hyphenated`) string representation. + def to_s(hyphenated = true) + slice = to_slice + if hyphenated + String.new(36) do |buffer| + buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8 + slice[0, 4].hexstring(buffer + 0) + slice[4, 2].hexstring(buffer + 9) + slice[6, 2].hexstring(buffer + 14) + slice[8, 2].hexstring(buffer + 19) + slice[10, 6].hexstring(buffer + 24) + {36, 36} + end + else + slice.hexstring + end + end + + # Returns `true` if `other` string represents the same UUID, `false` otherwise. + def ==(other : String) + self == UUID.new other + end + + # Returns `true` if `other` 16-byte slice represents the same UUID, `false` otherwise. + def ==(other : Slice(UInt8)) + to_slice == other + end + + # Returns `true` if `other` static 16 bytes represent the same UUID, `false` otherwise. + def ==(other : StaticArray(UInt8, 16)) + self.==(Slice(UInt8).new other.to_unsafe, 16) + end + end +end From a9019b6a6cbd656ad55da016d8353726f2aabff9 Mon Sep 17 00:00:00 2001 From: Mirek Rusin Date: Fri, 3 Jun 2016 18:11:25 -0300 Subject: [PATCH 3/5] Updates. --- spec/std/uuid_spec.cr | 44 +++++++++++++++++++ src/uuid.cr | 67 ++++++++++++++++++++++------- src/uuid/empty.cr | 14 +++++++ src/uuid/rfc4122.cr | 88 ++++++++++++++++++++++++++++---------- src/uuid/string.cr | 98 +++++++++++++++++++++++++++++++++++++++++++ src/uuid/uuid.cr | 90 --------------------------------------- src/uuid/variant.cr | 62 +++++++++++++++++++++++++++ 7 files changed, 338 insertions(+), 125 deletions(-) create mode 100644 src/uuid/empty.cr create mode 100644 src/uuid/string.cr delete mode 100644 src/uuid/uuid.cr create mode 100644 src/uuid/variant.cr diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr index 6cf3771b68b8..b9c632d9504e 100644 --- a/spec/std/uuid_spec.cr +++ b/spec/std/uuid_spec.cr @@ -1,6 +1,19 @@ require "spec" describe "UUID" do + it "has working zero UUID" do + UUID.empty.should eq UUID.empty + UUID.empty.to_s.should eq "00000000-0000-0000-0000-000000000000" + UUID.empty.variant.should eq UUID::Variant::NCS + end + + it "doesn't overwrite empty" do + empty = UUID.empty + empty.should eq empty + empty.decode "a01a5a94-7b52-4ca8-b310-382436650336" + UUID.empty.should_not eq empty + end + it "can be built from strings" do UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") UUID.new("c20335c37f464126aae9f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") @@ -8,11 +21,24 @@ describe "UUID" do UUID.new("C20335C37F464126AAE9F665434AD12B").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") end + it "should have correct variant and version" do + UUID.new("C20335C37F464126AAE9F665434AD12B").variant.should eq UUID::Variant::RFC4122 + UUID.new("C20335C37F464126AAE9F665434AD12B").version.should eq UUID::Version::V4 + end + + it "supports different string formats" do + UUID.new("ee843b2656d8472bb3430b94ed9077ff").to_s.should eq "ee843b26-56d8-472b-b343-0b94ed9077ff" + UUID.new("3e806983-eca4-4fc5-b581-f30fb03ec9e5").to_s(UUID::Format::Hexstring).should eq "3e806983eca44fc5b581f30fb03ec9e5" + UUID.new("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").to_s(UUID::Format::URN).should eq "urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892" + end + it "compares to strings" do uuid = UUID.new "c3b46146eb794e18877b4d46a10d1517" ->{ uuid == "c3b46146eb794e18877b4d46a10d1517" }.call.should eq(true) ->{ uuid == "c3b46146-eb79-4e18-877b-4d46a10d1517" }.call.should eq(true) ->{ uuid == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(true) + ->{ uuid == "urn:uuid:C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(true) + ->{ uuid == "urn:uuid:c3b46146-eb79-4e18-877b-4d46a10d1517" }.call.should eq(true) ->{ UUID.new == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(false) end @@ -33,4 +59,22 @@ describe "UUID" do expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e 06c" } expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e-76c" } end + + it "should handle variant" do + uuid = UUID.new + expect_raises(ArgumentError) { uuid.variant = UUID::Variant::Unknown } + {% for variant in %w(NCS RFC4122 Microsoft Future) %} + uuid.variant = UUID::Variant::{{ variant.id }} + uuid.variant.should eq UUID::Variant::{{ variant.id }} + {% end %} + end + + it "should handle version" do + uuid = UUID.new + expect_raises(ArgumentError) { uuid.version = UUID::Version::Unknown } + {% for version in %w(1 2 3 4 5) %} + uuid.version = UUID::Version::V{{ version.id }} + uuid.version.should eq UUID::Version::V{{ version.id }} + {% end %} + end end diff --git a/src/uuid.cr b/src/uuid.cr index b2ca2cd3bbe1..48a373249684 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -5,19 +5,58 @@ require "./uuid/*" # # Supports custom variants with arbitrary 16 bytes as well as (RFC 4122)[https://www.ietf.org/rfc/rfc4122.txt] variant # versions. -module UUID - # Raises `ArgumentError` if string `value` at index `i` doesn't contain hex digit followed by another hex digit. - # TODO: Move to String#digit?(...) - def self.string_has_hex_pair_at!(value : String, i) - unless value[i].hex? && value[i + 1].hex? - 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 - - def self.new(*args) - ::UUID::UUID.new *args +struct UUID + # Internal representation. + @data = StaticArray(UInt8, 16).new + + # Generates RFC 4122 v4 UUID. + def initialize + initialize Version::V4 + end + + # Generates UUID from static 16-`bytes`. + def initialize(bytes : StaticArray(UInt8, 16)) + @data = bytes + end + + # Creates UUID from 16-`bytes` slice. + def initialize(bytes : Slice(UInt8)) + raise ArgumentError.new "Invalid bytes length #{bytes.size}, expected 16." if bytes.size != 16 + @data.to_unsafe.copy_from bytes + end + + # Creates UUID from string `value`. See `UUID#decode(value : String)` for details on supported string formats. + def initialize(value : String) + decode value + end + + # Returns 16-byte slice. + def to_slice + Slice(UInt8).new to_unsafe, 16 + end + + # Returns unsafe pointer to 16-bytes. + def to_unsafe + @data.to_unsafe + end + + # Writes hyphenated format string to `io`. + def to_s(io : IO) + io << to_s + end + + # Returns `true` if `other` string represents the same UUID, `false` otherwise. + def ==(other : String) + self == UUID.new other + end + + # Returns `true` if `other` 16-byte slice represents the same UUID, `false` otherwise. + def ==(other : Slice(UInt8)) + to_slice == other + end + + # Returns `true` if `other` static 16 bytes represent the same UUID, `false` otherwise. + def ==(other : StaticArray(UInt8, 16)) + self.==(Slice(UInt8).new other.to_unsafe, 16) end end diff --git a/src/uuid/empty.cr b/src/uuid/empty.cr new file mode 100644 index 000000000000..f83524cbd24a --- /dev/null +++ b/src/uuid/empty.cr @@ -0,0 +1,14 @@ +struct UUID + # Empty UUID. + @@empty_bytes = StaticArray(UInt8, 16).new { 0_u8 } + + # Returns empty UUID (aka nil UUID where all bytes are set to `0`). + def self.empty + UUID.new @@empty_bytes + end + + # Resets UUID to an empty one. + def empty! + @bytes = @@empty_bytes + end +end diff --git a/src/uuid/rfc4122.cr b/src/uuid/rfc4122.cr index ecea8bb845fe..500d92f5aae3 100644 --- a/src/uuid/rfc4122.cr +++ b/src/uuid/rfc4122.cr @@ -1,35 +1,81 @@ # Support for RFC 4122 UUID variant. -module UUID::RFC4122 - enum RFC4122Version - Unknown - V1 - V2 - V3 - V4 - V5 +struct UUID + # RFC 4122 UUID variant versions. + enum Version + # Unknown version. + Unknown = 0 + + # Version 1 - date-time and MAC address. + V1 = 1 + + # Version 2 - DCE security. + V2 = 2 + + # Version 3 - MD5 hash and namespace. + V3 = 3 + + # Version 4 - random. + V4 = 4 + + # Version 5 - SHA1 hash and namespace. + V5 = 5 end - # Generates RFC UUID variant with specified format `version`. - def initialize(version : RFC4122Version) + # Generates RFC 4122 UUID `variant` with specified `version`. + def initialize(version : Version) case version - when RFC4122Version::V4 - @data.to_unsafe.copy_from SecureRandom.random_bytes(16).to_unsafe, 16 - @data[6] = (@data[6] & 0x0f) | 0x40 - @data[8] = (@data[8] & 0x3f) | 0x80 + when Version::V4 + @data = SecureRandom.random_bytes(16) + variant = Variant::RFC4122 + version = Version::V4 + else + raise ArgumentError.new "Creating #{version} not supported." + end + end + + # Returns version based on provided 6th `byte` (0-indexed). + def self.byte_version(byte : UInt8) + case byte >> 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 byte with encoded `version` for provided 6th `byte` (0-indexed) for known versions. + # For `Version::Unknown` `version` raises `ArgumentError`. + def self.byte_version(byte : UInt8, version : Version) : UInt8 + if version != Version::Unknown + (byte & 0xf) | (version.to_u8 << 4) else - raise ArgumentError.new "Unsupported version #{version}." + raise ArgumentError.new "Can't set unknown version." end end - {% for version in %w(1 2 3 4 5) %} + # Returns version based on RFC 4122 format. See also `UUID#variant`. + def version + UUID.byte_version @data[6] + end + + # Sets variant to a specified `value`. Doesn't set variant (see `UUID#variant=(value : Variant)`). + def version=(value : Version) + @data[6] = UUID.byte_version @data[6], value + end + + {% for v in %w(1 2 3 4 5) %} - def v{{ version.id }}? - rfc4122_version == RFC4122Version::V{{ version.id }} + # Returns `true` if UUID looks like V{{ v.id }}, `false` otherwise. + def v{{ v.id }}? + variant == Variant::RFC4122 && version == RFC4122::Version::V{{ v.id }} end - def v{{ version.id }}! - unless v{{ version.id }}? - raise Error.new("Invalid RFC 4122 UUID version #{rfc_4122}, expected V{{ version.id }}.") + # Returns `true` if UUID looks like 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 diff --git a/src/uuid/string.cr b/src/uuid/string.cr new file mode 100644 index 000000000000..05fa73369405 --- /dev/null +++ b/src/uuid/string.cr @@ -0,0 +1,98 @@ +struct UUID + # Raises `ArgumentError` if string `value` at index `i` doesn't contain hex digit followed by another hex digit. + # TODO: Move to String#digits?(base, offset, size) or introduce strict String#[index, size].to_u8(base)! which doesn't + # allow non-digits. The problem it solves is that " 1".to_u8(16) is fine but if it appears inside hexstring + # it's not correct and there should be stdlib function to support it, without a need to build this kind of + # helpers. + def self.string_has_hex_pair_at!(value : String, i) + unless value[i].hex? && value[i + 1].hex? + 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 + + # String format. + enum Format + Hyphenated + Hexstring + URN + 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 decode(value : String) + 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| + ::UUID.string_has_hex_pair_at! value, offset + @data[i] = value[offset, 2].to_u8(16) + end + when 32 # Hexstring + 16.times do |i| + ::UUID.string_has_hex_pair_at! value, i * 2 + @data[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| + ::UUID.string_has_hex_pair_at! value, offset + @data[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 + end + + #  Returns string in specified `format`. + def to_s(format = Format::Hyphenated) + slice = to_slice + case format + when Format::Hyphenated + String.new(36) do |buffer| + buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8 + slice[0, 4].hexstring(buffer + 0) + slice[4, 2].hexstring(buffer + 9) + slice[6, 2].hexstring(buffer + 14) + slice[8, 2].hexstring(buffer + 19) + slice[10, 6].hexstring(buffer + 24) + {36, 36} + end + when Format::Hexstring + slice.hexstring + when Format::URN + String.new(45) do |buffer| + buffer.copy_from "urn:uuid:".to_unsafe, 9 + (buffer + 9).copy_from to_s.to_unsafe, 36 + {45, 45} + end + else + raise ArgumentError.new "Unexpected format #{format}." + end + end + + # Same as `UUID#decode(value : String)`, returns `self`. + def <<(value : String) + decode value + self + end + + # Same as `UUID#variant=(value : Variant)`, returns `self`. + def <<(value : Variant) + variant = value + self + end + + # Same as `UUID#version=(value : Version)`, returns `self`. + def <<(value : Version) + version = value + self + end +end diff --git a/src/uuid/uuid.cr b/src/uuid/uuid.cr deleted file mode 100644 index 548317b29d03..000000000000 --- a/src/uuid/uuid.cr +++ /dev/null @@ -1,90 +0,0 @@ -module UUID - struct UUID - include ::UUID::RFC4122 - - # Internal representation. - @data = StaticArray(UInt8, 16).new - - # Generates UUID (RFC 4122 v4). - def initialize - initialize RFC4122Version::V4 - end - - # Creates UUID from any 16 `bytes` slice. - def initialize(bytes : Slice(UInt8)) - raise ArgumentError.new "Invalid bytes length #{bytes.size}, expected 16." if bytes.size != 16 - @data.to_unsafe.copy_from bytes - end - - # Creates UUID from (optionally hyphenated) string `value`. - def initialize(value : String) - case value.size - when 36 # with hyphens - [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| - ::UUID.string_has_hex_pair_at! value, offset - @data[i] = value[offset, 2].to_u8(16) - end - when 32 # without hyphens - 16.times do |i| - ::UUID.string_has_hex_pair_at! value, i * 2 - @data[i] = value[i * 2, 2].to_u8(16) - end - else - raise ArgumentError.new "Invalid string length #{value.size} for UUID, expected 32 (hex) or 36 (hyphenated hex)." - end - end - - # Returns 16-byte slice of this UUID. - def to_slice - Slice(UInt8).new to_unsafe, 16 - end - - # Returns unsafe pointer to 16-byte slice of this UUID. - def to_unsafe - @data.to_unsafe - end - - # Writes hyphenated string representation for this UUID. - def to_s(io : IO) - io << to_s(true) - end - - # Returns (optionally `hyphenated`) string representation. - def to_s(hyphenated = true) - slice = to_slice - if hyphenated - String.new(36) do |buffer| - buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8 - slice[0, 4].hexstring(buffer + 0) - slice[4, 2].hexstring(buffer + 9) - slice[6, 2].hexstring(buffer + 14) - slice[8, 2].hexstring(buffer + 19) - slice[10, 6].hexstring(buffer + 24) - {36, 36} - end - else - slice.hexstring - end - end - - # Returns `true` if `other` string represents the same UUID, `false` otherwise. - def ==(other : String) - self == UUID.new other - end - - # Returns `true` if `other` 16-byte slice represents the same UUID, `false` otherwise. - def ==(other : Slice(UInt8)) - to_slice == other - end - - # Returns `true` if `other` static 16 bytes represent the same UUID, `false` otherwise. - def ==(other : StaticArray(UInt8, 16)) - self.==(Slice(UInt8).new other.to_unsafe, 16) - end - end -end diff --git a/src/uuid/variant.cr b/src/uuid/variant.cr new file mode 100644 index 000000000000..4a1b5d5d5816 --- /dev/null +++ b/src/uuid/variant.cr @@ -0,0 +1,62 @@ +struct UUID + # UUID variants. + enum Variant + # Unknown (ie. custom, your own). + Unknown + + # Reserved by the NCS for backward compatibility. + NCS + + # As described in the RFC4122 Specification (default). + RFC4122 + + # Reserved by Microsoft for backward compatibility. + Microsoft + + # Reserved for future expansion. + Future + end + + # Returns UUID variant based on provided 8th `byte` (0-indexed). + def self.byte_variant(byte : UInt8) + case + when byte & 0x80 == 0x00 + Variant::NCS + when byte & 0xc0 == 0x80 + Variant::RFC4122 + when byte & 0xe0 == 0xc0 + Variant::Microsoft + when byte & 0xe0 == 0xe0 + Variant::Future + else + Variant::Unknown + end + end + + # Returns byte with encoded `variant` based on provided 8th `byte` (0-indexed) for known variants. + # For `Variant::Unknown` `variant` raises `ArgumentError`. + def self.byte_variant(byte : UInt8, variant : Variant) : UInt8 + case variant + when Variant::NCS + byte & 0x7f + when Variant::RFC4122 + (byte & 0x3f) | 0x80 + when Variant::Microsoft + (byte & 0x1f) | 0xc0 + when Variant::Future + (byte & 0x1f) | 0xe0 + else + raise ArgumentError.new "Can't set unknown variant." + end + end + + # Returns UUID variant. + def variant + UUID.byte_variant @data[8] + end + + # Sets UUID variant to specified `value`. + def variant=(value : Variant) + @data[8] = UUID.byte_variant @data[8], value + end +end From 638e8e0ae054e6626e1b76ba55833fc6728e3c66 Mon Sep 17 00:00:00 2001 From: Jack Thorne Date: Thu, 16 Jun 2016 22:36:26 +0000 Subject: [PATCH 4/5] remove uuid from prelude --- spec/std/uuid_spec.cr | 1 + src/prelude.cr | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr index b9c632d9504e..1ebf2a192623 100644 --- a/spec/std/uuid_spec.cr +++ b/spec/std/uuid_spec.cr @@ -1,4 +1,5 @@ require "spec" +require "uuid" describe "UUID" do it "has working zero UUID" do diff --git a/src/prelude.cr b/src/prelude.cr index 88078350b718..a797ec392e76 100644 --- a/src/prelude.cr +++ b/src/prelude.cr @@ -70,5 +70,4 @@ require "thread" require "time" require "tuple" require "union" -require "uuid" require "value" From 5fc9a49d9023301ffcd9934d140f3b5c4bce016c Mon Sep 17 00:00:00 2001 From: Jack Thorne Date: Fri, 1 Jul 2016 10:18:49 -0700 Subject: [PATCH 5/5] wip --- spec/std/uuid_spec.cr | 133 +++++++++++---------- src/uuid.cr | 270 +++++++++++++++++++++++++++++++++++++----- src/uuid/empty.cr | 14 --- src/uuid/rfc4122.cr | 85 ------------- src/uuid/string.cr | 98 --------------- src/uuid/variant.cr | 62 ---------- 6 files changed, 306 insertions(+), 356 deletions(-) delete mode 100644 src/uuid/empty.cr delete mode 100644 src/uuid/rfc4122.cr delete mode 100644 src/uuid/string.cr delete mode 100644 src/uuid/variant.cr diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr index 1ebf2a192623..fdd4bb4e2878 100644 --- a/spec/std/uuid_spec.cr +++ b/spec/std/uuid_spec.cr @@ -2,80 +2,83 @@ require "spec" require "uuid" describe "UUID" do - it "has working zero UUID" do - UUID.empty.should eq UUID.empty - UUID.empty.to_s.should eq "00000000-0000-0000-0000-000000000000" - UUID.empty.variant.should eq UUID::Variant::NCS - end + it "#initialize with no args" do + expected_uuid = UUID.new - it "doesn't overwrite empty" do - empty = UUID.empty - empty.should eq empty - empty.decode "a01a5a94-7b52-4ca8-b310-382436650336" - UUID.empty.should_not eq empty + # expected_uuid.variant.should eq UUID::Variant::RFC4122 + # expected_uuid.version.should eq 4_u8 end - it "can be built from strings" do - UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") - UUID.new("c20335c37f464126aae9f665434ad12b").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") - UUID.new("C20335C3-7F46-4126-AAE9-F665434AD12B").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") - UUID.new("C20335C37F464126AAE9F665434AD12B").should eq("c20335c3-7f46-4126-aae9-f665434ad12b") - end + it "#initialize from strings" do + expected_uuid = UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b") - it "should have correct variant and version" do - UUID.new("C20335C37F464126AAE9F665434AD12B").variant.should eq UUID::Variant::RFC4122 - UUID.new("C20335C37F464126AAE9F665434AD12B").version.should eq UUID::Version::V4 + expected_uuid.should eq UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b") + expected_uuid.should eq UUID.new("C20335C3-7F46-4126-AAE9-F665434AD12B") + expected_uuid.should eq UUID.new("c20335c37f464126aae9f665434ad12b") + expected_uuid.should eq UUID.new("C20335C37F464126AAE9F665434AD12B") + expected_uuid.should eq UUID.new("urn:uuid:c20335c3-7f46-4126-aae9-f665434ad12b") end - it "supports different string formats" do - UUID.new("ee843b2656d8472bb3430b94ed9077ff").to_s.should eq "ee843b26-56d8-472b-b343-0b94ed9077ff" - UUID.new("3e806983-eca4-4fc5-b581-f30fb03ec9e5").to_s(UUID::Format::Hexstring).should eq "3e806983eca44fc5b581f30fb03ec9e5" - UUID.new("1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892").to_s(UUID::Format::URN).should eq "urn:uuid:1ed1ee2f-ef9a-4f9c-9615-ab14d8ef2892" - end + # it "#initialize from array" do + # expected_uuid = UUID.new([0_u8, 1_u8, 2_u8, 3_u8, 4_u8, 5_u8, 6_u8, 7_u8, + # 8_u8, 9_u8, 10_u8, 11_u8, 12_u8, 13_u8, 14_u8, 15_u8]) - it "compares to strings" do - uuid = UUID.new "c3b46146eb794e18877b4d46a10d1517" - ->{ uuid == "c3b46146eb794e18877b4d46a10d1517" }.call.should eq(true) - ->{ uuid == "c3b46146-eb79-4e18-877b-4d46a10d1517" }.call.should eq(true) - ->{ uuid == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(true) - ->{ uuid == "urn:uuid:C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(true) - ->{ uuid == "urn:uuid:c3b46146-eb79-4e18-877b-4d46a10d1517" }.call.should eq(true) - ->{ UUID.new == "C3B46146-EB79-4E18-877B-4D46A10D1517" }.call.should eq(false) - end + # # expected_uuid.variant.should eq UUID::Variant::RFC4122 + # # expected_uuid.version.should eq 4_u8 + # expected_uuid.to_s.should eq "00010203-0405-0607-0809-0a0b0c0d0e0f" + # end - it "fails on invalid arguments when creating" do - expect_raises(ArgumentError) { UUID.new "" } - 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 + # it "#initialize has the correct version and variant" do + # expected_uuid = UUID.new - it "fails when comparing to invalid strings" do - expect_raises(ArgumentError) { UUID.new == "" } - expect_raises(ArgumentError) { UUID.new == "d1fb9189-7013-4915-a8b1-07cfc83bca3U" } - expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e0 6c" } - expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e 06c" } - expect_raises(ArgumentError) { UUID.new == "2ab8ffc8f58749e197eda3e3d14e-76c" } - end + # expected_uuid.variant.should eq UUID::Variant::RFC4122 + # expected_uuid.version.should eq 4_u8 + # end - it "should handle variant" do - uuid = UUID.new - expect_raises(ArgumentError) { uuid.variant = UUID::Variant::Unknown } - {% for variant in %w(NCS RFC4122 Microsoft Future) %} - uuid.variant = UUID::Variant::{{ variant.id }} - uuid.variant.should eq UUID::Variant::{{ variant.id }} - {% end %} - end + # it "#initialize with args has the correct version and variant" do + # expected_string_uuid = UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b") - it "should handle version" do - uuid = UUID.new - expect_raises(ArgumentError) { uuid.version = UUID::Version::Unknown } - {% for version in %w(1 2 3 4 5) %} - uuid.version = UUID::Version::V{{ version.id }} - uuid.version.should eq UUID::Version::V{{ version.id }} - {% end %} - end + # expected_string_uuid.variant.should eq UUID::Variant::RFC4122 + # expected_string_uuid.version.should eq 4_u8 + # end + + # it "#== with String" do end + # it "#== with Array" do end + # it "#to_a" do end + # it "#to_s" do end + # it "#to_s with format" do end + + # it "#version" do end + # it "#version=" do end + # it "#variant" do end + # it "#variant=" do end + + # it "class level decodes to UUID" do + # expected_uuid = UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b") + + # expected_uuid.should eq UUID.decode("c20335c3-7f46-4126-aae9-f665434ad12b") + # expected_uuid.should eq UUID.decode("c20335c37f464126aae9f665434ad12b") + # expected_uuid.should eq UUID.decode("urn:uuid:c20335c3-7f46-4126-aae9-f665434ad12b") + # end + + # it "#decodes to UUID" do + # expected_uuid = UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b") + + # actual_hypenated_uuid = UUID.new + # actual_hexstring_uuid = UUID.new + # actual_urn_uuid = UUID.new + + # expected_uuid.should eq actual_hypenated_uuid.decode("c20335c3-7f46-4126-aae9-f665434ad12b") + # expected_uuid.should eq actual_hexstring_uuid.decode("c20335c37f464126aae9f665434ad12b") + # expected_uuid.should eq actual_urn_uuid.decode("urn:uuid:c20335c3-7f46-4126-aae9-f665434ad12b") + # end + + # it "#encodes to string in different formats" do + # expected_uuid = UUID.new("c20335c3-7f46-4126-aae9-f665434ad12b") + + # expected_uuid.encode.should eq "c20335c3-7f46-4126-aae9-f665434ad12b" + # expected_uuid.encode(:hyphenated).should eq "c20335c3-7f46-4126-aae9-f665434ad12b" + # expected_uuid.encode(:hexstring).should eq "c20335c37f464126aae9f665434ad12b" + # expected_uuid.encode(:urn).should eq "urn:uuid:c20335c3-7f46-4126-aae9-f665434ad12b" + # end end diff --git a/src/uuid.cr b/src/uuid.cr index 48a373249684..17039ab11f8e 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -1,62 +1,268 @@ require "secure_random" -require "./uuid/*" # Universally Unique IDentifier. # # Supports custom variants with arbitrary 16 bytes as well as (RFC 4122)[https://www.ietf.org/rfc/rfc4122.txt] variant # versions. struct UUID - # Internal representation. - @data = StaticArray(UInt8, 16).new + enum Variant # UUID variants. + Unknown # Unknown (ie. custom, your own). + NCS # Reserved by the NCS for backward compatibility. + RFC4122 # As described in the RFC4122 Specification (default). + Microsoft # Reserved by Microsoft for backward compatibility. + Future # Reserved for future expansion. + end - # Generates RFC 4122 v4 UUID. + # Generates a new `UUID` in the RFC 4122 v4 UUID format. + # ``` + # uuid = UUID.new + # uuid.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + ## TODO: should we set version? def initialize - initialize Version::V4 + @bytes = SecureRandom.random_bytes(16).to_a + self.variant = Variant::RFC4122 + self.version = 4_u8 end - # Generates UUID from static 16-`bytes`. - def initialize(bytes : StaticArray(UInt8, 16)) - @data = bytes + # Generates a new `UUID` from a 16-`bytes` Array.. + # ``` + # arr = [0_u8, 1_u8, 2_u8, 3_u8, 4_u8, 5_u8, 6_u8, 7_u8, 8_u8, 9_u8, 10_u8, 11_u8, 12_u8, 13_u8, 14_u8, 15_u8] + # uuid = UUID.new(arr) + # uuid.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + ## TODO: use instance_sizeof() to see if it can be coersed into UUID + ## TODO: should we set version? + def initialize(new_bytes : Array(UInt8)) + raise ArgumentError.new "Invalid bytes length #{new_bytes.size}, expected 16." if new_bytes.size != 16 + @bytes = new_bytes + # self.variant = Variant::RFC4122 + # self.version = 4_u8 end - # Creates UUID from 16-`bytes` slice. - def initialize(bytes : Slice(UInt8)) - raise ArgumentError.new "Invalid bytes length #{bytes.size}, expected 16." if bytes.size != 16 - @data.to_unsafe.copy_from bytes + # Generates a new `UUID` from string `value`. + # See `UUID#decode(value : String)` for details on supported string formats. + # ``` + # value = "c20335c3-7f46-4126-aae9-f665434ad12b" + # uuid = UUID.new(value) + # uuid.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + def initialize(new_bytes : String) + @bytes = Array(UInt8).new + decode new_bytes end - # Creates UUID from string `value`. See `UUID#decode(value : String)` for details on supported string formats. - def initialize(value : String) - decode value + # Returns `true` if `other` string represents the same UUID, `false` otherwise. + # ``` + # value1 = "c20335c3-7f46-4126-aae9-f665434ad12b" + # value2 = "ee843b26-56d8-472b-b343-0b94ed9077ff" + # uuid1 = UUID.new(value1) + # uuid2 = UUID.new(value2) + # uuid1 == uuid1.to_s # => true + # uuid1 == uuid2.to_s # => false + # ``` + def ==(other : String) + self == UUID.new other end - # Returns 16-byte slice. - def to_slice - Slice(UInt8).new to_unsafe, 16 + # Returns `true` if `other` static 16 bytes represent the same UUID, `false` otherwise. + # ``` + # arr1 = [0_u8, 1_u8, 2_u8, 3_u8, 4_u8, 5_u8, 6_u8, 7_u8, 8_u8, 9_u8, 10_u8, 11_u8, 12_u8, 13_u8, 14_u8, 15_u8] + # arr2 = [15_u8, 14_u8, 13_u8, 12_u8, 11_u8, 10_u8, 9_u8, 8_u8, 7_u8, 6_u8, 5_u8, 4_u8, 3_u8, 2_u8, 1_u8, 0_u8] + # uuid1 = UUID.new(arr1) + # uuid2 = UUID.new(arr2) + # uuid1 == uuid1.to_a # => true + # uuid1 == uuid2.to_a # => false + # ``` + def ==(other : Array(UInt8)) + self.to_a == other end - # Returns unsafe pointer to 16-bytes. - def to_unsafe - @data.to_unsafe + # Returns the internal Representation of the UUID as an `Array(UInt8)`. + # ``` + # arr = [0_u8, 1_u8, 2_u8, 3_u8, 4_u8, 5_u8, 6_u8, 7_u8, 8_u8, 9_u8, 10_u8, 11_u8, 12_u8, 13_u8, 14_u8, 15_u8] + # uuid = UUID.new(arr) + # uuid.to_a # => [0_u8, 1_u8, 2_u8, 3_u8, 4_u8, 5_u8, 6_u8, 7_u8, 8_u8, 9_u8, 10_u8, 11_u8, 12_u8, 13_u8, 14_u8, 15_u8] + # ``` + def to_a + @bytes end - # Writes hyphenated format string to `io`. + # Writes a hyphenated format String to `io`. + # See `UUID#encode(format : Symbol)` for details on String encoding. + # ``` + # uuid = UUID.new + # uuid.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` def to_s(io : IO) - io << to_s + io << encode end - # Returns `true` if `other` string represents the same UUID, `false` otherwise. - def ==(other : String) - self == UUID.new other + # Writes a String to `io` with *args*. + # See `UUID#encode(format : Symbol)` for details on String encoding. + # ``` + # uuid = UUID.new + # uuid.to_s(:hexstring) # => "c20335c37f464126aae9f665434ad12b" + # ``` + def to_s(io : IO, format : Symbol) + io << encode(format) end - # Returns `true` if `other` 16-byte slice represents the same UUID, `false` otherwise. - def ==(other : Slice(UInt8)) - to_slice == other + # Returns version based on RFC 4122 format. See also `UUID#variant`. + # ``` + # uuid = UUID.new + # uuid.version # => 4 + # ``` + def version + @bytes[6] >> 4 end - # Returns `true` if `other` static 16 bytes represent the same UUID, `false` otherwise. - def ==(other : StaticArray(UInt8, 16)) - self.==(Slice(UInt8).new other.to_unsafe, 16) + # Sets version to a specified `value`. + # Doesn't set variant (see `UUID#variant=(value : UInt8)`). + # ``` + # uuid = UUID.new + # uuid.version = 4_u8 + # uuid.version # => 4 + # ``` + def version=(version : UInt8) + @bytes[6] = (@bytes[6] & 0xf) | (version << 4) + end + + # Returns `UUID` variant. + # Values for this are documented at `UUID#Variant` + # ``` + # uuid = UUID.new + # uuid.variant # => UUID::Variant::RFC4122 + # ``` + 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 + + # Sets `UUID` variant to specified `variant`. + # Values for this are documented at `UUID#Variant` + # ``` + # uuid = UUID.new + # uuid.variant = UUID::Variant::RFC4122 + # uuid.variant # => UUID::Variant::RFC4122 + # ``` + def variant=(variant : Variant) + case variant + 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 + end + + + # Generates a `UUID` from a formatted `UUID` String. + # See `UUID#encode(format : Symbol)` for details on String encoding. + # ``` + # uuid1 = UUID.decode("c20335c3-7f46-4126-aae9-f665434ad12b") + # uuid2 = UUID.decode("c20335c37f464126aae9f665434ad12b") + # uuid3 = UUID.decode("urn:uuid:c20335c3-7f46-4126-aae9-f665434ad12b") + # uuid1.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # uuid2.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # uuid3.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + def self.decode(value : String) + value = value.delete("urn:uuid:") if value.starts_with? "urn:uuid:" + value = value.delete('-') if [value[8], value[13], value[18], value[23]] == Array.new(4, '-') + raise ArgumentError.new "Invalid UUID provided" if value.size != 32 + + results = Array(UInt8).new + char_iterator = value.each_char + 16.times do |index| + char_pair = [char_iterator.next.to_s, char_iterator.next.to_s] + # raise ArgumentError.new "Invalid UUID char format" unless char_pair.all?(&.hex?) + results << char_pair.join.to_u8(16) + end + + new(results) + end + + # Generates a `UUID` from a formatted `UUID` String. + # See `UUID#encode(format : Symbol)` for details on String encoding. + # Hyphenated Format + # ``` + # uuid = UUID.new + # uuid.to_s # => "ee843b26-56d8-472b-b343-0b94ed9077ff" + # uuid.decode("c20335c3-7f46-4126-aae9-f665434ad12b") + # uuid.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + # Hexstring Format + # ``` + # uuid = UUID.new + # uuid.to_s # => "ee843b26-56d8-472b-b343-0b94ed9077ff" + # uuid.decode("c20335c37f464126aae9f665434ad12b") + # uuid.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + # URN Format + # ``` + # uuid = UUID.new + # uuid.to_s # => "ee843b26-56d8-472b-b343-0b94ed9077ff" + # uuid.decode("urn:uuid:c20335c3-7f46-4126-aae9-f665434ad12b") + # uuid.to_s # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + def decode(value : String) + value = value[0..8] if value.starts_with? "urn:uuid:" + value = value.delete('-') if [value[8], value[13], value[18], value[23]] == Array.new(4, '-') + raise ArgumentError.new "Invalid UUID provided" if value.size != 32 + + # value.each_char.each_combination(2) do |pair| + # raise ArgumentError.new "Invalid UUID char format" unless char_pair.all?(&.hex?) + value.each_char do |char| + @bytes << char.to_i(16).to_u8 + end + # end + + @bytes + end + + # Generates a String representing a `UUID` + # Hyphenated Format contains '-' between after the 2nd, 3rd, 4th, 5th byte + # ``` + # uuid = UUID.new + # uuid.encode # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # uuid.encode(:hyphenated) # => "c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + # Hexstring Format is a 32 character String of "0-9" or "a-z" or "A-Z" + # ``` + # uuid = UUID.new + # uuid.encode(:hexstring) # => "c20335c37f464126aae9f665434ad12b" + # ``` + # URN Format begins with `"urn:uuid:"` and then a Hyphonated Fromat String + # ``` + # uuid = UUID.new + # uuid.encode(:urn) # => "urn:uuid:c20335c3-7f46-4126-aae9-f665434ad12b" + # ``` + def encode(format = :hyphenated) + case format + when :hyphenated + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" % @bytes + when :hexstring + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" % @bytes + when :urn + "urn:uuid:%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" % @bytes + else + raise ArgumentError.new "Unexpected format #{format}." + end end end diff --git a/src/uuid/empty.cr b/src/uuid/empty.cr deleted file mode 100644 index f83524cbd24a..000000000000 --- a/src/uuid/empty.cr +++ /dev/null @@ -1,14 +0,0 @@ -struct UUID - # Empty UUID. - @@empty_bytes = StaticArray(UInt8, 16).new { 0_u8 } - - # Returns empty UUID (aka nil UUID where all bytes are set to `0`). - def self.empty - UUID.new @@empty_bytes - end - - # Resets UUID to an empty one. - def empty! - @bytes = @@empty_bytes - end -end diff --git a/src/uuid/rfc4122.cr b/src/uuid/rfc4122.cr deleted file mode 100644 index 500d92f5aae3..000000000000 --- a/src/uuid/rfc4122.cr +++ /dev/null @@ -1,85 +0,0 @@ -# Support for RFC 4122 UUID variant. -struct UUID - # RFC 4122 UUID variant versions. - enum Version - # Unknown version. - Unknown = 0 - - # Version 1 - date-time and MAC address. - V1 = 1 - - # Version 2 - DCE security. - V2 = 2 - - # Version 3 - MD5 hash and namespace. - V3 = 3 - - # Version 4 - random. - V4 = 4 - - # Version 5 - SHA1 hash and namespace. - V5 = 5 - end - - # Generates RFC 4122 UUID `variant` with specified `version`. - def initialize(version : Version) - case version - when Version::V4 - @data = SecureRandom.random_bytes(16) - variant = Variant::RFC4122 - version = Version::V4 - else - raise ArgumentError.new "Creating #{version} not supported." - end - end - - # Returns version based on provided 6th `byte` (0-indexed). - def self.byte_version(byte : UInt8) - case byte >> 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 byte with encoded `version` for provided 6th `byte` (0-indexed) for known versions. - # For `Version::Unknown` `version` raises `ArgumentError`. - def self.byte_version(byte : UInt8, version : Version) : UInt8 - if version != Version::Unknown - (byte & 0xf) | (version.to_u8 << 4) - else - raise ArgumentError.new "Can't set unknown version." - end - end - - # Returns version based on RFC 4122 format. See also `UUID#variant`. - def version - UUID.byte_version @data[6] - end - - # Sets variant to a specified `value`. Doesn't set variant (see `UUID#variant=(value : Variant)`). - def version=(value : Version) - @data[6] = UUID.byte_version @data[6], value - end - - {% for v in %w(1 2 3 4 5) %} - - # Returns `true` if UUID looks like V{{ v.id }}, `false` otherwise. - def v{{ v.id }}? - variant == Variant::RFC4122 && version == RFC4122::Version::V{{ v.id }} - end - - # Returns `true` if UUID looks like 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 diff --git a/src/uuid/string.cr b/src/uuid/string.cr deleted file mode 100644 index 05fa73369405..000000000000 --- a/src/uuid/string.cr +++ /dev/null @@ -1,98 +0,0 @@ -struct UUID - # Raises `ArgumentError` if string `value` at index `i` doesn't contain hex digit followed by another hex digit. - # TODO: Move to String#digits?(base, offset, size) or introduce strict String#[index, size].to_u8(base)! which doesn't - # allow non-digits. The problem it solves is that " 1".to_u8(16) is fine but if it appears inside hexstring - # it's not correct and there should be stdlib function to support it, without a need to build this kind of - # helpers. - def self.string_has_hex_pair_at!(value : String, i) - unless value[i].hex? && value[i + 1].hex? - 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 - - # String format. - enum Format - Hyphenated - Hexstring - URN - 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 decode(value : String) - 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| - ::UUID.string_has_hex_pair_at! value, offset - @data[i] = value[offset, 2].to_u8(16) - end - when 32 # Hexstring - 16.times do |i| - ::UUID.string_has_hex_pair_at! value, i * 2 - @data[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| - ::UUID.string_has_hex_pair_at! value, offset - @data[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 - end - - #  Returns string in specified `format`. - def to_s(format = Format::Hyphenated) - slice = to_slice - case format - when Format::Hyphenated - String.new(36) do |buffer| - buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8 - slice[0, 4].hexstring(buffer + 0) - slice[4, 2].hexstring(buffer + 9) - slice[6, 2].hexstring(buffer + 14) - slice[8, 2].hexstring(buffer + 19) - slice[10, 6].hexstring(buffer + 24) - {36, 36} - end - when Format::Hexstring - slice.hexstring - when Format::URN - String.new(45) do |buffer| - buffer.copy_from "urn:uuid:".to_unsafe, 9 - (buffer + 9).copy_from to_s.to_unsafe, 36 - {45, 45} - end - else - raise ArgumentError.new "Unexpected format #{format}." - end - end - - # Same as `UUID#decode(value : String)`, returns `self`. - def <<(value : String) - decode value - self - end - - # Same as `UUID#variant=(value : Variant)`, returns `self`. - def <<(value : Variant) - variant = value - self - end - - # Same as `UUID#version=(value : Version)`, returns `self`. - def <<(value : Version) - version = value - self - end -end diff --git a/src/uuid/variant.cr b/src/uuid/variant.cr deleted file mode 100644 index 4a1b5d5d5816..000000000000 --- a/src/uuid/variant.cr +++ /dev/null @@ -1,62 +0,0 @@ -struct UUID - # UUID variants. - enum Variant - # Unknown (ie. custom, your own). - Unknown - - # Reserved by the NCS for backward compatibility. - NCS - - # As described in the RFC4122 Specification (default). - RFC4122 - - # Reserved by Microsoft for backward compatibility. - Microsoft - - # Reserved for future expansion. - Future - end - - # Returns UUID variant based on provided 8th `byte` (0-indexed). - def self.byte_variant(byte : UInt8) - case - when byte & 0x80 == 0x00 - Variant::NCS - when byte & 0xc0 == 0x80 - Variant::RFC4122 - when byte & 0xe0 == 0xc0 - Variant::Microsoft - when byte & 0xe0 == 0xe0 - Variant::Future - else - Variant::Unknown - end - end - - # Returns byte with encoded `variant` based on provided 8th `byte` (0-indexed) for known variants. - # For `Variant::Unknown` `variant` raises `ArgumentError`. - def self.byte_variant(byte : UInt8, variant : Variant) : UInt8 - case variant - when Variant::NCS - byte & 0x7f - when Variant::RFC4122 - (byte & 0x3f) | 0x80 - when Variant::Microsoft - (byte & 0x1f) | 0xc0 - when Variant::Future - (byte & 0x1f) | 0xe0 - else - raise ArgumentError.new "Can't set unknown variant." - end - end - - # Returns UUID variant. - def variant - UUID.byte_variant @data[8] - end - - # Sets UUID variant to specified `value`. - def variant=(value : Variant) - @data[8] = UUID.byte_variant @data[8], value - end -end