diff --git a/lib/uuid.rb b/lib/uuid.rb index dac0af1..3d1f7d7 100644 --- a/lib/uuid.rb +++ b/lib/uuid.rb @@ -11,6 +11,7 @@ require 'tmpdir' require 'socket' require 'macaddr' +require 'digest/sha1' ## @@ -194,6 +195,55 @@ def self.validate(uuid) uuid =~ /\A(urn:uuid:)?[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}\z/i end + ## + # Generate a pseudo MAC address because we have no pure-ruby way + # to know the MAC address of the NIC this system uses. Note + # that cheating with pseudo arresses here is completely legal: + # see Section 4.5 of RFC4122 for details. + # + # This implementation is shamelessly stolen from + # https://github.com/spectra/ruby-uuid/blob/master/uuid.rb + # Thanks spectra. + # + def pseudo_mac_address + sha1 = ::Digest::SHA1.new + 256.times do + r = [rand(0x100000000)].pack "N" + sha1.update r + end + str = sha1.digest + r = rand 14 # 20-6 + node = str[r, 6] || str + if RUBY_VERSION >= "1.9.0" + nnode = node.bytes.to_a + nnode[0] |= 0x01 + node = '' + nnode.each { |s| node << s.chr } + else + node[0] |= 0x01 # multicast bit + end + node.bytes.collect{|b|b.to_s(16)}.join.hex & 0x7FFFFFFFFFFF + end + + ## + # Uses system calls to get a mac address + # + def iee_mac_address + begin + Mac.addr.gsub(/:|-/, '').hex & 0x7FFFFFFFFFFF + rescue + 0 + end + end + + ## + # return iee_mac_address if available, pseudo_mac_address otherwise + # + def mac_address + return iee_mac_address unless iee_mac_address == 0 + return pseudo_mac_address + end + ## # Create a new UUID generator. You really only need to do this once. def initialize @@ -205,8 +255,8 @@ def initialize if state_file && File.size?(state_file) then next_sequence else - @mac = Mac.addr.gsub(/:|-/, '').hex & 0x7FFFFFFFFFFF - fail "Cannot determine MAC address from any available interface, tried with #{Mac.addr}" if @mac == 0 + @mac = mac_address + fail "Cannot determine MAC address from any available interface, tried with #{mac_address}" if @mac == 0 @sequence = rand 0x10000 if state_file diff --git a/test/test-uuid.rb b/test/test-uuid.rb index c4ea1c9..d28ce5f 100644 --- a/test/test-uuid.rb +++ b/test/test-uuid.rb @@ -4,6 +4,7 @@ # License:: MIT and/or Creative Commons Attribution-ShareAlike require 'test/unit' +require 'rubygems' require 'uuid' class TestUUID < Test::Unit::TestCase @@ -31,8 +32,7 @@ def test_with_no_state_file assert !UUID.state_file end - def test_instance_generate - uuid = UUID.new + def validate_uuid_generator(uuid) assert_match(/\A[\da-f]{32}\z/i, uuid.generate(:compact)) assert_match(/\A[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\z/i, @@ -44,8 +44,13 @@ def test_instance_generate e = assert_raise ArgumentError do uuid.generate :unknown end - assert_equal 'invalid UUID format :unknown', e.message + + end + + def test_instance_generate + uuid = UUID.new + validate_uuid_generator(uuid) end def test_class_generate @@ -108,5 +113,15 @@ class << bar = UUID.new assert_equal foo.sequence + 1, bar.sequence end + def test_pseudo_random_mac_address + uuid_gen = UUID.new + def Mac.addr; '00:00:00:00:00:00'; end + assert uuid_gen.iee_mac_address == 0 + [:compact, :default, :urn].each do |format| + assert UUID.validate(uuid_gen.generate(format)), format.to_s + end + validate_uuid_generator(uuid_gen) + end + end