|
| 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