Skip to content
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

Merged
merged 75 commits into from
Nov 24, 2017
Merged
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
3b1bae7
Add `UUID`.
mirek Jun 1, 2016
f0bb4d8
Rearrange structure to separate RFC4122 logic.
mirek Jun 2, 2016
a9019b6
Updates.
mirek Jun 3, 2016
cbfd749
Merge branch 'master' into uuid
mirek Aug 16, 2016
a2e23a3
Merge branch 'master' into uuid
mirek Oct 9, 2016
3d69d69
Merge remote-tracking branch 'origin/master' into uuid
May 23, 2017
133e84d
remove uuid from prelude; update for moden build for crystal
May 23, 2017
c6a9604
Merge remote-tracking branch 'origin/master' into uuid
May 23, 2017
0ca59d6
Use `should` and `eq` instead of checking against true
jkthorne May 24, 2017
2d881d9
fix typo
jkthorne May 24, 2017
e48a7d5
Doc fix
jkthorne May 24, 2017
a48e4fb
Change the implimintation of UUID::EMPTY
jkthorne May 25, 2017
64e64f7
Update Docs
jkthorne May 25, 2017
df3fd11
Merge branch 'master' into uuid
May 25, 2017
55ad17a
Change formatting form to_s to methods
May 26, 2017
c1cf14d
Merge branch 'uuid' of github.com:wontruefree/crystal into uuid
May 26, 2017
8295cfa
wip
May 26, 2017
b7c17f3
wip
May 30, 2017
c6b781c
remove trasition to Random
jkthorne May 31, 2017
8fee1b6
add bytes to initialize
jkthorne May 31, 2017
f72b57b
remove class level variant functions
jkthorne May 31, 2017
7139655
Merge branch 'master' into uuid
jkthorne May 31, 2017
f3fce3d
update hex pair check to use String std
Jun 1, 2017
dfbc0c1
Merge branch 'master' into uuid
jkthorne Jun 3, 2017
a71f569
Merge branch 'uuid' of github.com:wontruefree/crystal into uuid
jkthorne Jun 3, 2017
41302ca
Merge remote-tracking branch 'upstream/master' into uuid
Jun 6, 2017
0f7d36f
cleanup some code left over from wip
Jun 7, 2017
9242f73
Merge remote-tracking branch 'upstream/master' into uuid
Jun 9, 2017
baa9277
move to initialize from new and be more explicit about the setup
Jun 9, 2017
e34308e
Merge branch 'uuid' of github.com:wontruefree/crystal into uuid
Jun 9, 2017
5dce117
Merge remote-tracking branch 'upstream/master' into uuid
Jun 20, 2017
ad9a37f
Merge remote-tracking branch 'upstream/master' into uuid
Jun 23, 2017
509ff75
Merge remote-tracking branch 'upstream/master' into uuid
Jun 27, 2017
e55718d
Merge remote-tracking branch 'upstream/master' into uuid
Jul 5, 2017
4cfd90e
Merge remote-tracking branch 'upstream/master' into uuid
Jul 7, 2017
79f5b7d
Merge remote-tracking branch 'upstream/master' into uuid
Jul 28, 2017
7b4b9ad
condense uuid files
Jul 28, 2017
2cae97a
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Aug 6, 2017
f369a34
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Aug 8, 2017
b325f3d
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Aug 13, 2017
ac2d176
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Aug 24, 2017
9290bf7
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Sep 16, 2017
6044754
Move uuid functions into one file.
jkthorne Sep 16, 2017
627a8e1
linter
jkthorne Sep 17, 2017
cc14710
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Oct 8, 2017
8ff542e
remove secure_random
jkthorne Oct 8, 2017
8639668
linter
jkthorne Oct 8, 2017
8d25980
remove Random uuid
jkthorne Oct 9, 2017
8062232
remove `<<` and extra `==` methods
jkthorne Oct 9, 2017
8f47d38
revert linter commits
jkthorne Oct 10, 2017
d79b51f
use safer copy for UUID initilaize
jkthorne Oct 10, 2017
9828bd7
add safer copy with slice intialize
jkthorne Oct 11, 2017
0366f78
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Oct 11, 2017
409fe5b
rework UUID intialize and add more tests
jkthorne Oct 12, 2017
e3bee29
change string to be made without unsafe methods
jkthorne Oct 12, 2017
49a6372
reorder methods and make to_slice safe
jkthorne Oct 17, 2017
17ddbbf
add specs for default initialize
jkthorne Oct 17, 2017
f282061
remove periods from comments
jkthorne Oct 20, 2017
a80ff09
remove periods from error messages
jkthorne Oct 20, 2017
b4f1340
remove whitespace
jkthorne Oct 21, 2017
4669f9a
revert comment changes
jkthorne Oct 21, 2017
1c3e811
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Oct 22, 2017
3addefc
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Oct 27, 2017
30db4ad
changing to Random for performance
jkthorne Oct 27, 2017
eec5ece
Revert "changing to Random for performance"
jkthorne Oct 27, 2017
494d20b
add random over new
jkthorne Oct 27, 2017
6a25156
move class methods together
jkthorne Oct 27, 2017
264f729
Merge remote-tracking branch 'upstream/master' into uuid
jkthorne Oct 30, 2017
6a143fa
change uuid to use secure random
jkthorne Oct 30, 2017
1c8db2b
Make UUID immutable, and not overwrite the version and variant
RX14 Nov 7, 2017
3712820
Docs fixes
RX14 Nov 7, 2017
f6f6070
Add constructor to initialize from UUID
RX14 Nov 8, 2017
122f84b
Remove magic equality with String
RX14 Nov 8, 2017
c496177
UUID#to_s cleanup
RX14 Nov 8, 2017
9c49e7b
Clarify doc for UUID.new(UUID, ...)
RX14 Nov 8, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions spec/std/random_spec.cr
Original file line number Diff line number Diff line change
@@ -260,11 +260,4 @@ describe "Random" do
hex.should eq("9fd857f462831002ffffffffffffffff0000000000000000e88d3a30db4e730021b8a5e33b020000362f518e0700000062da")
end
end

describe "uuid" do
it "gets uuid" do
uuid = TestRNG.new(RNG_DATA_8).uuid
uuid.should eq("ea990000-7f80-4fff-aa99-00007f80ffff")
end
end
end
122 changes: 122 additions & 0 deletions spec/std/uuid_spec.cr
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
28 changes: 0 additions & 28 deletions src/random.cr
Original file line number Diff line number Diff line change
@@ -373,34 +373,6 @@ module Random
random_bytes(n).hexstring
end

# Generates a UUID (Universally Unique Identifier).
Copy link
Contributor

@Sija Sija Oct 25, 2017

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:

# It generates a random v4 UUID (Universally Unique Identifier).
#
# See `UUID.new`
def uuid : UUID
  UUID.new
end

Copy link
Member

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 uses Random::Secure. And for good reason.

#
# It generates a random v4 UUID. See
# [RFC 4122 Section 4.4](https://tools.ietf.org/html/rfc4122#section-4.4)
# for the used algorithm and its implications.
#
# ```
# Random::Secure.uuid # => "a4e319dd-a778-4a51-804e-66a07bc63358"
# ```
#
# It is recommended to use the secure `Random::Secure` as a source or another
# cryptographically quality PRNG such as `Random::ISAAC` or ChaCha20.
def uuid : String
bytes = random_bytes(16)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80

String.new(36) do |buffer|
buffer[8] = buffer[13] = buffer[18] = buffer[23] = 45_u8
bytes[0, 4].hexstring(buffer + 0)
bytes[4, 2].hexstring(buffer + 9)
bytes[6, 2].hexstring(buffer + 14)
bytes[8, 2].hexstring(buffer + 19)
bytes[10, 6].hexstring(buffer + 24)
{36, 36}
end
end

# See `#rand`.
def self.rand : Float64
DEFAULT.rand
209 changes: 209 additions & 0 deletions src/uuid.cr
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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean "if UUID looks as"?

Copy link
Member

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

The 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