# encoding: UTF-8
#!/usr/bin/env ruby
### http://mput.dip.jp/mput/uuid.txt

# Copyright(c) 2005 URABE, Shyouhei.
#
# Permission is hereby granted, free of  charge, to any person obtaining a copy
# of  this code, to  deal in  the code  without restriction,  including without
# limitation  the rights  to  use, copy,  modify,  merge, publish,  distribute,
# sublicense, and/or sell copies of the code, and to permit persons to whom the
# code is furnished to do so, subject to the following conditions:
#
#        The above copyright notice and this permission notice shall be
#        included in all copies or substantial portions of the code.
#
# THE  CODE IS  PROVIDED "AS  IS",  WITHOUT WARRANTY  OF ANY  KIND, EXPRESS  OR
# IMPLIED,  INCLUDING BUT  NOT LIMITED  TO THE  WARRANTIES  OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE  AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHOR  OR  COPYRIGHT  HOLDER BE  LIABLE  FOR  ANY  CLAIM, DAMAGES  OR  OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF  OR IN CONNECTION WITH  THE CODE OR THE  USE OR OTHER  DEALINGS IN THE
# CODE.
#
# 2009-02-20:  Modified by Pablo Lorenzoni <pablo@propus.com.br>  to  correctly
# include the version in the raw_bytes.


require 'digest/md5'
require 'digest/sha1'
require 'tmpdir'

# Pure ruby UUID generator, which is compatible with RFC4122
UUID = Struct.new :raw_bytes

class UUID
  private_class_method :new

  class << self
    def mask19 v, str # :nodoc
      nstr = str.bytes.to_a
      version = [0, 16, 32, 48, 64, 80][v]
      nstr[6] &= 0b00001111
      nstr[6] |= version
#     nstr[7] &= 0b00001111
#     nstr[7] |= 0b01010000
      nstr[8] &= 0b00111111
      nstr[8] |= 0b10000000
      str = ''
      nstr.each { |s| str << s.chr }
      str
    end

    def mask18 v, str # :nodoc
      version = [0, 16, 32, 48, 64, 80][v]
      str[6] &= 0b00001111
      str[6] |= version
#     str[7] &= 0b00001111
#     str[7] |= 0b01010000
      str[8] &= 0b00111111
      str[8] |= 0b10000000
      str
    end

    def mask v, str
      if RUBY_VERSION >= "1.9.0"
        return mask19 v, str
      else
        return mask18 v, str
      end
    end
    private :mask, :mask18, :mask19

    # UUID generation using SHA1. Recommended over create_md5.
    # Namespace object is another UUID, some of them are pre-defined below.
    def create_sha1 str, namespace
      sha1 = Digest::SHA1.new
      sha1.update namespace.raw_bytes
      sha1.update str
      sum = sha1.digest
      raw = mask 5, sum[0..15]
      ret = new raw
      ret.freeze
      ret
    end
    alias :create_v5 :create_sha1

    # UUID generation using MD5 (for backward compat.)
    def create_md5 str, namespace
      md5 = Digest::MD5.new
      md5.update namespace.raw_bytes
      md5.update str
      sum = md5.digest
      raw = mask 3, sum[0..16]
      ret = new raw
      ret.freeze
      ret
    end
    alias :create_v3 :create_md5

    # UUID  generation  using  random-number  generator.   From  it's  random
    # nature, there's  no warranty that  the created ID is  really universaly
    # unique.
    def create_random
      rnd = [
        rand(0x100000000),
        rand(0x100000000),
        rand(0x100000000),
        rand(0x100000000),
      ].pack "N4"
      raw = mask 4, rnd
      ret = new raw
      ret.freeze
      ret
    end
    alias :create_v4 :create_random

    def read_state fp       # :nodoc:
      fp.rewind
      Marshal.load fp.read
    end

    def write_state fp, c, m  # :nodoc:
      fp.rewind
      str = Marshal.dump [c, m]
      fp.write str.force_encoding("utf-8")
    end

    private :read_state, :write_state
    STATE_FILE = 'ruby-uuid'

    # create  the "version  1" UUID  with current  system clock,  current UTC
    # timestamp, and the IEEE 802 address (so-called MAC address).
    #
    # Speed notice: it's slow.  It writes  some data into hard drive on every
    # invokation. If you want to speed  this up, try remounting tmpdir with a
    # memory based filesystem  (such as tmpfs).  STILL slow?  then no way but
    # rewrite it with c :)
    def create clock=nil, time=nil, mac_addr=nil
      c = t = m = nil
      Dir.chdir Dir.tmpdir do
        unless FileTest.exist? STATE_FILE then
          # 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.
          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
          k = rand 0x40000
          open STATE_FILE, 'w' do |fp|
            fp.flock IO::LOCK_EX
            write_state fp, k, node
            fp.chmod 0o777 # must be world writable
          end
        end
        open STATE_FILE, 'r+' do |fp|
          fp.flock IO::LOCK_EX
          c, m = read_state fp
          c = clock % 0x4000 if clock
          m = mac_addr if mac_addr
          t = time
          if t.nil? then
            # UUID epoch is 1582/Oct/15
            tt = Time.now
            t = tt.to_i*10000000 + tt.tv_usec*10 + 0x01B21DD213814000
          end
          c = c.succ # important; increment here
          write_state fp, c, m
        end
      end

      tl = t & 0xFFFF_FFFF
      tm = t >> 32
      tm = tm & 0xFFFF
      th = t >> 48
      th = th & 0x0FFF
      th = th | 0x1000
      cl = c & 0xFF
      ch = c & 0x3F00
      ch = ch >> 8
      ch = ch | 0x80
      pack tl, tm, th, cl, ch, m
    end
    alias :create_v1 :create

    # A  simple GUID  parser:  just ignores  unknown  characters and  convert
    # hexadecimal dump into 16-octet object.
    def parse obj
      str = obj.to_s.sub %r/\Aurn:uuid:/, ''
      str.gsub! %r/[^0-9A-Fa-f]/, ''
      raw = str[0..31].lines.to_a.pack 'H*'
      ret = new raw
      ret.freeze
      ret
    end

    # The 'primitive constructor' of this class
    # Note UUID.pack(uuid.unpack) == uuid
    def pack tl, tm, th, ch, cl, n
      raw = [tl, tm, th, ch, cl, n].pack "NnnCCa6"
      ret = new raw
      ret.freeze
      ret
    end
  end

  # The 'primitive deconstructor', or the dual to pack.
  # Note UUID.pack(uuid.unpack) == uuid
  def unpack
    raw_bytes.unpack "NnnCCa6"
  end

  # Generate the string representation (a.k.a GUID) of this UUID
  def to_s
    a = unpack
    tmp = a[-1].unpack 'C*'
    a[-1] = sprintf '%02x%02x%02x%02x%02x%02x', *tmp
    "%08x-%04x-%04x-%02x%02x-%s" % a
  end
  alias guid to_s

  # Convert into a RFC4122-comforming URN representation
  def to_uri
    "urn:uuid:" + self.to_s
  end
  alias urn to_uri

  # Convert into 128-bit unsigned integer
  # Typically a Bignum instance, but can be a Fixnum.
  def to_int
    tmp = self.raw_bytes.unpack "C*"
    tmp.inject do |r, i|
      r * 256 | i
    end
  end
  alias to_i to_int

  # Gets the version of this UUID
  # returns nil if bad version
  def version
    a = unpack
    v = (a[2] & 0xF000).to_s(16)[0].chr.to_i
    return v if (1..5).include? v
    return nil
  end

  # Two  UUIDs  are  said  to  be  equal if  and  only  if  their  (byte-order
  # canonicalized) integer representations are equivallent.  Refer RFC4122 for
  # details.
  def == other
    to_i == other.to_i
  end

  include Comparable
  # UUIDs are comparable (don't know what benefits are there, though).
  def <=> other
    to_s <=> other.to_s
  end

  # Pre-defined UUID Namespaces described in RFC4122 Appendix C.
  NameSpace_DNS = parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
  NameSpace_URL = parse "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
  NameSpace_OID = parse "6ba7b812-9dad-11d1-80b4-00c04fd430c8"
  NameSpace_X500 = parse "6ba7b814-9dad-11d1-80b4-00c04fd430c8"

  # The Nil UUID in RFC4122 Section 4.1.7
  Nil = parse "00000000-0000-0000-0000-000000000000"
end

__END__
if __FILE__ == $0 then
  require 'test/unit'

  class TC_UUID < Test::Unit::TestCase
    def test_v1
      u1 = UUID.create
      u2 = UUID.create
      assert_not_equal u1, u2
    end

    def test_v1_repeatability
      u1 = UUID.create 1, 2, "345678"
      u2 = UUID.create 1, 2, "345678"
      assert_equal u1, u2
    end

    def test_v3
      u1 = UUID.create_md5 "foo", UUID::NameSpace_DNS
      u2 = UUID.create_md5 "foo", UUID::NameSpace_DNS
      u3 = UUID.create_md5 "foo", UUID::NameSpace_URL
      assert_equal u1, u2
      assert_not_equal u1, u3
    end

    def test_v5
      u1 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
      u2 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
      u3 = UUID.create_sha1 "foo", UUID::NameSpace_URL
      assert_equal u1, u2
      assert_not_equal u1, u3
    end

    def test_v4
      # This test  is not  perfect, because the  random nature of  version 4
      # UUID  it is  not always  true that  the three  objects  below really
      # differ.  But  in real  life it's  enough to say  we're OK  when this
      # passes.
      u1 = UUID.create_random
      u2 = UUID.create_random
      u3 = UUID.create_random
      assert_not_equal u1.raw_bytes, u2.raw_bytes
      assert_not_equal u1.raw_bytes, u3.raw_bytes
      assert_not_equal u2.raw_bytes, u3.raw_bytes
    end

    def test_pack
      u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
                     "\000\300O\3240\310"
      assert_equal UUID::NameSpace_DNS, u1
    end

    def test_unpack
      tl, tm, th, cl, ch, m = UUID::NameSpace_DNS.unpack
      assert_equal 0x6ba7b810, tl
      assert_equal 0x9dad, tm
      assert_equal 0x11d1, th
      assert_equal 0x80, cl
      assert_equal 0xb4, ch
      assert_equal "\000\300O\3240\310", m
    end

    def test_parse
      u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
                     "\000\300O\3240\310"
      u2 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
      u3 = UUID.parse "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
      assert_equal u1, u2
      assert_equal u1, u3
    end

    def test_to_s
      u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
      assert_equal "6ba7b810-9dad-11d1-80b4-00c04fd430c8", u1.to_s
    end

    def test_to_i
      u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
      assert_equal 0x6ba7b8109dad11d180b400c04fd430c8, u1.to_i
    end

    def test_version
      u1 = UUID.create_v1
      assert_equal 1, u1.version
      u3 = UUID.create_v3 "foo", UUID::NameSpace_DNS
      assert_equal 3, u3.version
      u4 = UUID.create_v4
      assert_equal 4, u4.version
      u5 = UUID.create_v5 "foo", UUID::NameSpace_DNS
      assert_equal 5, u5.version
    end
  end
end
