From 8fcc5d7e6f98dc2dd0fbbe29fd795598af53e142 Mon Sep 17 00:00:00 2001 From: Jamie Gaskins Date: Wed, 13 Apr 2022 20:03:54 -0400 Subject: [PATCH 1/4] Add `UUID.parse?` to return nil on failure Previously, there was only `UUID.new(String)` to parse a UUID from a string, but it raised an exception on failure. This commit adds another implementation that returns `nil` instead, requiring the caller to handle the failure because it returns more than just a `UUID`. --- spec/std/uuid_spec.cr | 17 ++++++++++++++ src/uuid.cr | 53 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr index 7a5167f8f885..5086b7964f95 100644 --- a/spec/std/uuid_spec.cr +++ b/spec/std/uuid_spec.cr @@ -105,6 +105,23 @@ describe "UUID" do end end + describe "parsing strings" do + it "returns a properly parsed UUID" do + UUID.parse?("c20335c3-7f46-4126-aae9-f665434ad12b").to_s.should eq("c20335c3-7f46-4126-aae9-f665434ad12b") + end + + it "returns nil if it has the wrong number of characters" do + UUID.parse?("nope").should eq nil + end + + it "returns nil if it has incorrect characters" do + UUID.parse?("c20335c3-7f46-4126-aae9-f665434ad12?").should eq nil + UUID.parse?("lol!wut?-asdf-fork-typo-omglolwtfbbq").should eq nil + UUID.parse?("lol!wut?asdfforktypoomglolwtfbbq").should eq nil + UUID.parse?("urn:uuid:lol!wut?-asdf-fork-typo-omglolwtfbbq").should eq nil + 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) diff --git a/src/uuid.cr b/src/uuid.cr index ccc2ca5608a3..949669e36d4d 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -107,16 +107,59 @@ struct UUID new(bytes, variant, version) end + def self.parse?(value : String, variant = nil, version = nil) + bytes = uninitialized UInt8[16] + + case value.size + when 36 # Hyphenated + {8, 13, 18, 23}.each do |offset| + return if value[offset] != '-' + end + {0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34}.each_with_index do |offset, i| + if hex = hex_pair_at? value, offset + bytes[i] = hex + else + return + end + end + when 32 # Hexstring + 16.times do |i| + if hex = hex_pair_at? value, i * 2 + bytes[i] = hex + else + return + end + end + when 45 # URN + return 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| + if hex = hex_pair_at? value, offset + bytes[i] = hex + else + return + end + end + else + return + 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.hex_pair_at(value : String, i) : UInt8 + hex_pair_at?(value, i) || 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 + + # Parses 2 hex digits from `value` at index `i` and `i + 1`, returning `nil` + # if one or both are not actually hex digits. + private def self.hex_pair_at?(value : String, i) : UInt8? if (ch1 = value[i].to_u8?(16)) && (ch2 = value[i + 1].to_u8?(16)) ch1 * 16 + ch2 - else - 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 From ffa29a89505415260733efc8547571177461eab4 Mon Sep 17 00:00:00 2001 From: Jamie Gaskins Date: Fri, 15 Apr 2022 07:45:39 -0400 Subject: [PATCH 2/4] Combine error message into one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- src/uuid.cr | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uuid.cr b/src/uuid.cr index 949669e36d4d..e87195f5f793 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -149,10 +149,7 @@ struct UUID # Raises `ArgumentError` if string `value` at index `i` doesn't contain hex # digit followed by another hex digit. private def self.hex_pair_at(value : String, i) : UInt8 - hex_pair_at?(value, i) || 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(", ") + hex_pair_at?(value, i) || 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'" end # Parses 2 hex digits from `value` at index `i` and `i + 1`, returning `nil` From 89a627e2e492ad8ffa62607e0566fbc680863160 Mon Sep 17 00:00:00 2001 From: Jamie Gaskins Date: Sat, 16 Apr 2022 15:02:00 -0400 Subject: [PATCH 3/4] Add document string and return type to UUID.parse? --- src/uuid.cr | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/uuid.cr b/src/uuid.cr index e87195f5f793..0ddd85507b42 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -75,9 +75,9 @@ struct UUID 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. + # 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, raising an `ArgumentError` if the string fors not match any of these formats. def self.new(value : String, variant = nil, version = nil) bytes = uninitialized UInt8[16] @@ -107,7 +107,10 @@ struct UUID new(bytes, variant, version) end - def self.parse?(value : String, variant = nil, version = nil) + # 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, returning `nil` if the string does not match any of these formats. + def self.parse?(value : String, variant = nil, version = nil) : UUID? bytes = uninitialized UInt8[16] case value.size From b95197fa111d35c91a9d03b96c6ba8962c9859c5 Mon Sep 17 00:00:00 2001 From: Jamie Gaskins Date: Sat, 16 Apr 2022 15:06:31 -0400 Subject: [PATCH 4/4] Fix typo --- src/uuid.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uuid.cr b/src/uuid.cr index 0ddd85507b42..99d079e18a1c 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -77,7 +77,7 @@ struct UUID # 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, raising an `ArgumentError` if the string fors not match any of these formats. + # format, raising an `ArgumentError` if the string does not match any of these formats. def self.new(value : String, variant = nil, version = nil) bytes = uninitialized UInt8[16]