# encoding: UTF-8
#
# = uuid.rb - UUID generator
#
# Author:: Assaf Arkin  assaf@labnotes.org
#          Eric Hodel drbrain@segment7.net
# Copyright:: Copyright (c) 2005-2010 Assaf Arkin, Eric Hodel
# License:: MIT and/or Creative Commons Attribution-ShareAlike

require 'fileutils'
require 'thread'
require 'tmpdir'
require 'socket'
require 'macaddr'
require 'digest/sha1'
require 'tmpdir'


##
# = Generating UUIDs
#
# Call #generate to generate a new UUID. The method returns a string in one of
# three formats. The default format is 36 characters long, and contains the 32
# hexadecimal octets and hyphens separating the various value parts. The
# <tt>:compact</tt> format omits the hyphens, while the <tt>:urn</tt> format
# adds the <tt>:urn:uuid</tt> prefix.
#
# For example:
#
#   uuid = UUID.new
#
#   10.times do
#     p uuid.generate
#   end
#
# = UUIDs in Brief
#
# UUID (universally unique identifier) are guaranteed to be unique across time
# and space.
#
# A UUID is 128 bit long, and consists of a 60-bit time value, a 16-bit
# sequence number and a 48-bit node identifier.
#
# The time value is taken from the system clock, and is monotonically
# incrementing.  However, since it is possible to set the system clock
# backward, a sequence number is added.  The sequence number is incremented
# each time the UUID generator is started.  The combination guarantees that
# identifiers created on the same machine are unique with a high degree of
# probability.
#
# Note that due to the structure of the UUID and the use of sequence number,
# there is no guarantee that UUID values themselves are monotonically
# incrementing.  The UUID value cannot itself be used to sort based on order
# of creation.
#
# To guarantee that UUIDs are unique across all machines in the network,
# the IEEE 802 MAC address of the machine's network interface card is used as
# the node identifier.
#
# For more information see {RFC 4122}[http://www.ietf.org/rfc/rfc4122.txt].

class UUID

  # Version number.
  module Version
    version = "Dir[../../../share/rubygems-integration/all/specifications/uuid*.gemspec].to_s.match /.*-(.*)\.gemspec/"
    MAJOR = version[0]
    MINOR = version[1]
    PATCH = version[2]
    STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
  end

  VERSION = Version::STRING

  ##
  # Clock multiplier. Converts Time (resolution: seconds) to UUID clock
  # (resolution: 10ns)
  CLOCK_MULTIPLIER = 10000000

  ##
  # Clock gap is the number of ticks (resolution: 10ns) between two Ruby Time
  # ticks.
  CLOCK_GAPS = 100000

  ##
  # Version number stamped into the UUID to identify it as time-based.
  VERSION_CLOCK = 0x0100

  ##
  # Formats supported by the UUID generator.
  #
  # <tt>:default</tt>:: Produces 36 characters, including hyphens separating
  #                     the UUID value parts
  # <tt>:compact</tt>:: Produces a 32 digits (hexadecimal) value with no
  #                     hyphens
  # <tt>:urn</tt>:: Adds the prefix <tt>urn:uuid:</tt> to the default format
  FORMATS = {
    :compact => '%08x%04x%04x%04x%012x',
    :default => '%08x-%04x-%04x-%04x-%012x',
    :urn     => 'urn:uuid:%08x-%04x-%04x-%04x-%012x',
  }

  ##
  # MAC address (48 bits), sequence number and last clock
  STATE_FILE_FORMAT = 'SLLQ'

  @state_file = nil
  @mode = nil
  @uuid = nil

  ##
  # The access mode of the state file.  Set it with state_file.

  def self.mode
    @mode
  end

  def self.mode=(mode)
    @mode = mode
  end

  ##
  # Generates a new UUID string using +format+.  See FORMATS for a list of
  # supported formats.

  def self.generate(format = :default)
    @uuid ||= new
    @uuid.generate format
  end

  ##
  # Returns the UUID generator used by generate.  Useful if you need to mess
  # with it, e.g. force next sequence when forking (e.g. Unicorn, Resque):
  #
  # after_fork do
  #   UUID.generator.next_sequence
  # end
  def self.generator
    @uuid ||= new
  end

  ##
  # Call this to use a UUID Server.  Expects address to bind to (SOCKET_NAME is
  # a good default)
  def self.server=(address)
    @uuid = Client.new(address) unless Client === @uuid
  end

  ##
  # Creates an empty state file in #Dir.tmpdir/ruby-uuid or the windows common
  # application data directory using mode 0644.  Call with a different mode
  # before creating a UUID generator if you want to open access beyond your
  # user by default.
  #
  # If the default state dir is not writable, UUID falls back to ~/.ruby-uuid.
  #
  # State files are not portable across machines.
  def self.state_file(mode = 0644)
    return @state_file unless @state_file.nil?

    @mode = mode

    begin
      require 'Win32API'

      csidl_common_appdata = 0x0023
      path = 0.chr * 260
      get_folder_path = Win32API.new('shell32', 'SHGetFolderPath', 'LLLLP', 'L')
      get_folder_path.call 0, csidl_common_appdata, 0, 1, path

      state_dir = File.join(path.strip)
    rescue LoadError
      state_dir = Dir.tmpdir
    end

    @state_file = File.join(state_dir, 'ruby-uuid')

    if !File.writable?(state_dir) || (File.exist?(@state_file) && !File.writable?(@state_file)) then
      @state_file = File.expand_path('.ruby-uuid', '~')
    end

    @state_file
  end

  ##
  # Specify the path of the state file.  Use this if you need a different
  # location for your state file.
  #
  # Set to false if your system cannot use a state file (e.g. many shared
  # hosts).
  def self.state_file=(path)
    @state_file = path
    @mode ||= 0644
  end

  ##
  # Returns true if +uuid+ is in compact, default or urn formats.  Does not
  # validate the layout (RFC 4122 section 4) of the UUID.
  def self.validate(uuid)
    return true if uuid =~ /\A[\da-f]{32}\z/i
    return true if
      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
    @drift = 0
    @last_clock = (Time.now.to_f * CLOCK_MULTIPLIER).to_i
    @mutex = Mutex.new

    state_file = self.class.state_file
    if state_file && File.size?(state_file) then
      next_sequence
    else
      @mac = mac_address
      fail "Cannot determine MAC address from any available interface, tried with #{mac_address}" if @mac == 0
      @sequence = rand 0x10000

      # Ensure the mode is respected, even with a restrictive umask
      File.open(state_file, 'w') { |f| f.chmod(self.class.mode) } if state_file && !File.exist?(state_file)

      if state_file
        open_lock 'wb' do |io|
          write_state io
        end
      end
    end
  end

  ##
  # Generates a new UUID string using +format+.  See FORMATS for a list of
  # supported formats.
  def generate(format = :default)
    template = FORMATS[format]

    raise ArgumentError, "invalid UUID format #{format.inspect}" unless template

    # The clock must be monotonically increasing. The clock resolution is at
    # best 100 ns (UUID spec), but practically may be lower (on my setup,
    # around 1ms). If this method is called too fast, we don't have a
    # monotonically increasing clock, so the solution is to just wait.
    #
    # It is possible for the clock to be adjusted backwards, in which case we
    # would end up blocking for a long time. When backward clock is detected,
    # we prevent duplicates by asking for a new sequence number and continue
    # with the new clock.

    clock = @mutex.synchronize do
      clock = (Time.new.to_f * CLOCK_MULTIPLIER).to_i & 0xFFFFFFFFFFFFFFF0

      if clock > @last_clock then
        @drift = 0
        @last_clock = clock
      elsif clock == @last_clock then
        drift = @drift += 1

        if drift < 10000 then
          @last_clock += 1
        else
          Thread.pass
          nil
        end
      else
        next_sequence
        @last_clock = clock
      end
    end until clock

    template % [
        clock        & 0xFFFFFFFF,
       (clock >> 32) & 0xFFFF,
      ((clock >> 48) & 0xFFFF | VERSION_CLOCK),
      @sequence      & 0xFFFF,
      @mac           & 0xFFFFFFFFFFFF
    ]
  end

  ##
  # Updates the state file with a new sequence number.
  def next_sequence
    if self.class.state_file
      open_lock 'rb+' do |io|
        @mac, @sequence, @last_clock = read_state(io)

        io.rewind
        io.truncate 0

        @sequence += 1

        write_state io
      end
    else
      @sequence += 1
    end
  rescue Errno::ENOENT
    open_lock 'w' do |io|
      write_state io
    end
  ensure
    @last_clock = (Time.now.to_f * CLOCK_MULTIPLIER).to_i
    @drift = 0
  end

  def inspect
    mac = ("%012x" % @mac).scan(/[0-9a-f]{2}/).join(':')
    "MAC: #{mac}  Sequence: #{@sequence}"
  end

protected

  ##
  # Open the state file with an exclusive lock and access mode +mode+.
  def open_lock(mode)
    File.open self.class.state_file, mode, self.class.mode do |io|
      begin
        io.flock File::LOCK_EX
        yield io
      ensure
        io.flock File::LOCK_UN
      end
    end
  end

  ##
  # Read the state from +io+
  def read_state(io)
    mac1, mac2, seq, last_clock = io.read(32).unpack(STATE_FILE_FORMAT)
    mac = (mac1 << 32) + mac2

    return mac, seq, last_clock
  end


  ##
  # Write that state to +io+
  def write_state(io)
    mac2 =  @mac        & 0xffffffff
    mac1 = (@mac >> 32) & 0xffff

    io.write [mac1, mac2, @sequence, @last_clock].pack(STATE_FILE_FORMAT)
  end


  # You don't have to use this, it's just a good default.
  SOCKET_NAME ="/var/lib/uuid.sock"

  # With UUID server you don't have to worry about multiple processes
  # synchronizing over the state file, calling next_sequence when forking a
  # process and other things you're probably not worried about (because
  # statistically they're very unlikely to break your code).
  #
  # But if you are worried about and thought to yourself, "what would a simple
  # UUID server look like?", here's the answer.  The protocol is dead simple:
  # client sends a byte, server responds with a UUID.  Can use TCP or domain
  # sockets.
  class Server

    # Create new server.  Nothing interesting happens until you call listen.
    def initialize()
      @generator = UUID.new
    end

    # Start the server listening on the specific address.  Blocks and never
    # returns.  Address can be:
    # - A Socket object
    # - UNIX domain socket name (e.g. /var/run/uuid.sock, must start with /)
    # - IP address, colon, port (e.g. localhost:1337)
    def listen(address)
      sock = bind(address)
      while client = sock.accept
        Thread.start(client) do |socket|
          while socket.read 1
            socket.write @generator.generate
          end
        end
      end
    end

    # Returns UNIXServer or TCPServer from address.  Returns argument if not a
    # string, so can pass through (see #listen).
    def bind(address)
      return address unless String === address
      if address[0] == ?/
        if File.exist?(address)
          raise ArgumentError, "#{address} is not a socket" unless File.socket?(address)
          File.unlink(address)
        end
        sock = UNIXServer.new(address)
        File.chmod 0666, address
      elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/
        sock = TCPServer.new($1, $2.to_i)
      else
        raise ArgumentError, "Don't know how to bind #{address}"
      end
      sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) if defined?(TCP_NODELAY)
      sock
    end

  end


  # Every server needs a client.  Client provides you with the single ultimate
  # method: #generate.  Typically you'll use this instead of the local UUID
  # generator:
  #   UUID.server = UUID::SOCKET_NAME
  class Client

    def initialize(address)
      @socket = connect(address)
      at_exit { close }
    end

    # Talks to server and returns new UUID in specified format.
    def generate(format = :default)
      @socket.write "\0"
      uuid = @socket.read(36)
      return uuid if format == :default
      template = FORMATS[format]
      raise ArgumentError, "invalid UUID format #{format.inspect}" unless template
      template % uuid.split("-").map { |p| p.to_i(16) }
    end

    # Returns UNIXSocket or TCPSocket from address.  Returns argument if not a
    # string, so can pass through.
    def connect(address)
      return address unless String === address
      if address[0] == ?/
        sock = UNIXSocket.new(address)
      elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/
        sock = TCPSocket.new($1, $2.to_i)
      else
        raise ArgumentError, "Don't know how to connect to #{address}"
      end
      sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) if defined?(TCP_NODELAY)
      sock
    end

    def next_sequence #:nodoc: Stubbed to do nothing.
    end

    def inspect
      @socket ? "Server on #{Socket.unpack_sockaddr_in(@socket.getsockname).reverse!.join(':')}" : "Connection closed"
    end

    # Close the socket.
    def close
      @socket.shutdown if @socket
      @socket = nil
    end

  end

end
