ManageIQ/manageiq-smartstate

View on GitHub
lib/metadata/util/win32/ms-registry.rb

Summary

Maintainability
D
2 days
Test Coverage
B
80%
# encoding: US-ASCII

require 'binary_struct'
require 'miq_unicode'
require 'manageiq/gems/pending'
require 'util/miq-xml'
require 'util/xml/xml_hash'

# Constants
DEBUG_PRINT = false
DEBUG_UNHANDLED_DATA = false
DEBUG_LOG_PERFORMANCE = false
DEBUG_FILE_READS = false

class MSRegHive
  using ManageIQ::UnicodeString

  attr_reader :fileLoadTime, :fileParseTime, :digitalProductKeys, :xmlNode

  # Size of the HBIN data (as well as initiale REGF) segments
  HBIN_SIZE = 0x1000
  # All data offsets in the registry DO NOT include the first block (REGF) which
  # is 0x1000 (same as HBIN) and the 4 byte 'hbin' signature
  REG_DATA_OFFSET = HBIN_SIZE + 0x4

  def initialize(path, hiveName, xmlNode, fs = "M:/", filter = nil)
    @RegPath = path.gsub(/^"/, "").gsub(/"$/, "")
    @hiveName = hiveName
    @xmlNode = xmlNode
    @fs = fs if fs.kind_of?(MiqFS)
    @expandEnv = {'%SystemDrive%' => 'C:', "%SystemRoot%" => "\\Windows", "%ProgramFiles%" => "\\Program Files"}
    @fileLoadTime, @fileParseTime = nil, nil
    @ccsIdx = 1     # CurrentControlSet default index
    @ccsName = "controlset%03d" % @ccsIdx
    @stats = {:cache_hits => 0, :file_reads => 0, :bytes_read => 0}
    @hbin = {}

    # Load up filters
    @filter_value = {}
    @filter = init_filters(filter)

    # Collect DigitalProductKeys as we find them for processing later
    @digitalProductKeys = []
  end

  def init_filters(filter)
    if filter.nil?
      @filter_value = nil if @filter_value.empty?
      return nil
    end
    filters = filter.collect { |f| create_filter_hash(f) }
    filters.compact!
    filters = nil if filters.empty?
    @filter_value = nil if @filter_value.empty?
    filters
  end

  def create_filter_hash(filter)
    if filter.kind_of?(Hash)
      nh = filter
      nh[:key] = nh[:key].downcase.split("/")
      nh[:value].each { |v| @filter_value[v.downcase] = true } if nh[:value].kind_of?(Array)
    else
      nh = {:key => filter.downcase.split("/")}
    end
    nh[:key_path] = nh[:key].join('\\')
    nh[:depth] = nh[:depth].to_i
    nh
  end

  def close
    # Force memory cleanup
    @hbin = nil
    GC.start
  end

  def parseHives
    startTime = Time.now

    # Reads in the registry file and does some basic validation
    validateRegFile(File.join(@RegPath, @hiveName))

    @fileLoadTime = Time.now - startTime
    $log.info "Registry Load/Validate time = #{@fileLoadTime} sec" if DEBUG_LOG_PERFORMANCE

    startTime = Time.now
    pre_process

    # Start parsing the registry based on the data offset stored in the first record
    if @hiveName == 'ntuser.dat'
      parseRecord(@hiveHash[:data_offset], @xmlNode, nil, 0)
    else
      parseRecord(@hiveHash[:data_offset], @xmlNode, @hiveName, 0)
    end

    post_process

    @fileParseTime = Time.now - startTime
    parseStats = "Registry Parsing time = #{@fileParseTime} sec.  registry segments loaded:[#{@hbin.length}]" # if DEBUG_LOG_PERFORMANCE
    parseStats += "  Stats:[#{@stats.inspect}]" if DEBUG_FILE_READS
    $log.info parseStats
  end

  def pre_process
    # Determine what System/ControlSet00? to use when CurrentControlSet is
    # referenced and update the filter list.
    determine_current_control_set if @hiveName == "system"

    # Load environment variables to be used to "expand string" (REG_EXPAND_SZ) resolution.
    # load_environment_variables
  end

  def post_process
    if @hiveName == "system"
      ccsNode = MIQRexml.findRegElement("HKEY_LOCAL_MACHINE\\SYSTEM\\#{@ccsName}", @xmlNode.root)
      if ccsNode
        $log.debug "Changing [#{@ccsName}] to CurrentControlSet"
        ccsNode.add_attribute(:keyname, 'CurrentControlSet')
      end
    end
  end

  def load_environment_variables
    $log.debug "Determining ControlControlSet index"
    save_filters = @filter
    @filter = [create_filter_hash("#{@ccsName}/Control/Session Manager/Environment".downcase.split("/"))]
    # Start parsing the registry based on the data offset stored in the first record
    ccsNode = MiqXml.newNode
    parseRecord(@hiveHash[:data_offset], ccsNode, @hiveName, 0)
    @filter = save_filters

    # ccsNode.write(STDOUT,0)
    # @expandEnv = {"%SystemRoot%"=>"\\Windows", "%ProgramFiles%"=>"\\Program Files"}
  end

  def determine_current_control_set
    $log.debug "Determining ControlControlSet index"
    save_filters = @filter
    @filter = [create_filter_hash('select')]
    # Start parsing the registry based on the data offset stored in the first record
    # ccsNode = MiqXml.newNode(nil, REXML)
    ccsNode = XmlHash::Document.new("ccs")
    parseRecord(@hiveHash[:data_offset], ccsNode, @hiveName, 0)
    # idx = ccsNode.find_first("//value[@name\"Current\"]")
    @ccsIdx = 0
    ccsNode.elements[1].each_element_with_attribute(:name, "Current") { |e| @ccsIdx = e.text }
    @ccsIdx = 1 if @ccsIdx == 0
    @ccsName = "controlset%03d" % @ccsIdx
    @filter = save_filters
    # Search through the filter list and change any "CurrentControlSet" values to the proper idx
    if @filter
      @filter.each do |a1|
        if a1[:key][0] == "currentcontrolset"
          a1[:key][0] = @ccsName
          a1[:key_path] = a1[:key].join('\\')
        end
      end
    end
    $log.debug "ControlControlSet index will be set to [#{@ccsIdx}]"
  end

  def parseRecord(offset, xmlNode, fqName, level)
    type = read_buffer(offset, 1).downcase
    $log.debug sprintf("TYPE = [%s] at offset [0x%08x]", type, offset + REG_DATA_OFFSET) if DEBUG_PRINT
    begin
      send("parseRecord#{type}", offset, xmlNode, fqName, level)
    rescue => err
      $log.warn sprintf("Unhandled type encountered [%s] at file offset [0x%08X].  Msg:[#{err}]", type, offset + REG_DATA_OFFSET) if DEBUG_UNHANDLED_DATA
    end
  end

  def checkFilters(subKey, fqName, level)
    return true if @filter.nil?  # If there are no filters get out
    match = false
    # allNil = true
    alevel = level - 1

    @filter.each do |f|
      #      $log.debug "Filer [#{f[level]}]"
      #      allNil = false unless f[level].nil?
      if f[:key][alevel].nil? && fqName.downcase.index(f[:key_path])
        match = true if f[:depth].to_i == 0

        filter_depth = f[:depth] - 1 + f[:key].length
        if filter_depth >= level
          # $log.fatal "REG FILTER 1 fqName:[#{fqName.downcase}] - f[path]:[#{f[:key].join('\\')}] - depth:[#{filter_depth}] -- level:[#{level}]"
          match = true
          break
        end
      end
      if match == false && !f[:key][alevel].nil? && f[:key][alevel] == subKey
        match = true
        break
      end
    end
    #    $log.debug "match [#{match}]  allNil [#{allNil}]"
    #    return true if allNil == true # There were no filters specified at this depth
    match
  end

  def parseRecordnk(offset, xmlNode, fqName, level)
    nkHash = REGISTRY_STRUCT_NK.decode(read_buffer(offset, SIZEOF_REGISTRY_STRUCT_NK))
    # Convert the type from hex to text
    nkHash[:type_display] = typeToString(nkHash[:type])
    # Get the keyname which is just beyond the structure
    nkHash[:keyname] = read_buffer(offset + SIZEOF_REGISTRY_STRUCT_NK, nkHash[:name_length] - 1).chomp("\0")

    DumpHash.SortPrint(nkHash, :NK)

    # $log.debug "parseRecordNK [#{xmlNode}] [#{xmlNode.class}] [#{nkHash[:keyname]}] [#{nkHash[:type_display]}]"
    if nkHash[:type_display] == :SUB
      level += 1
      if fqName.nil?
        fqName = nkHash[:keyname].chomp
      else
        fqName += "\\#{nkHash[:keyname].chomp}"
      end
      # $log.debug "Fully Q Name: [#{level}]  [#{nkHash['keyname'].chomp}]  [#{fqName}]"
      # Check sub-directory filters
      # return unless checkFilters(nkHash['keyname'].chomp.downcase, level-1)
      cf = checkFilters(nkHash[:keyname].chomp.downcase, fqName, level)
      # $log.debug "Fully Q Name: [#{"%5s" % cf}] [#{fqName}]  [#{level}]"
      # $log.debug "Fully Q Name: [#{fqName}]  [#{level}]"
      return unless cf
      # xmlSubNode = xmlNode
      xmlSubNode = xmlNode.add_element(:key, :keyname => nkHash[:keyname].chomp, :fqname => fqName)
      # on_start_element(:key, {:keyname=>nkHash[:keyname].chomp, :fqname=>fqName})
    else
      xmlSubNode = xmlNode
    end

    # Process all values
    if nkHash[:num_values] > 0
      vkOffset = nkHash[:values_offset]
      nkHash[:num_values].times do
        vkHash = REGISTRY_STRUCT_VK_OFFSET.decode(read_buffer(vkOffset, SIZEOF_REGISTRY_STRUCT_VK_OFFSET))
        parseRecord(vkHash[:offset_vk], xmlSubNode, fqName, level)
        vkOffset += SIZEOF_REGISTRY_STRUCT_VK_OFFSET
      end
    end

    # Process all subkeys
    if nkHash[:num_subkeys] > 0
      parseRecord(nkHash[:subkeys_offset], xmlSubNode, fqName, level)
    end

    # on_end_element(:key)
  end

  def parseRecordri(offset, xmlNode, fqName, level)
    #        $log.debug "parseRecordRI at offset #{offset}"
    riHash = REGISTRY_STRUCT_RI.decode(read_buffer(offset, SIZEOF_REGISTRY_STRUCT_RI))

    DumpHash.SortPrint(riHash, :RI)

    if riHash[:num_keys] > 0
      key_offset = offset + SIZEOF_REGISTRY_STRUCT_RI
      riHash[:num_keys].times do
        hash = REGISTRY_STRUCT_RI_OFFSET.decode(read_buffer(key_offset, SIZEOF_REGISTRY_STRUCT_RI_OFFSET))
        parseRecord hash[:offset_ri], xmlNode, fqName, level
        key_offset += SIZEOF_REGISTRY_STRUCT_RI_OFFSET
      end
    end
  end

  def parseRecordlf(offset, xmlNode, fqName, level)
    # $log.debug "parseRecordLF at offset #{offset}"
    lfHash = REGISTRY_STRUCT_LF.decode(read_buffer(offset, SIZEOF_REGISTRY_STRUCT_LF))

    if lfHash[:num_keys] > 0
      key_offset = offset + SIZEOF_REGISTRY_STRUCT_LF
      lfHash[:num_keys].times do
        hash = REGISTRY_STRUCT_LF_HASH.decode(read_buffer(key_offset, SIZEOF_REGISTRY_STRUCT_LF_HASH))
        parseRecord hash[:offset_nk], xmlNode, fqName, level
        key_offset += SIZEOF_REGISTRY_STRUCT_LF_HASH
      end
    end
  end

  def parseRecordlh(offset, xmlNode, fqName, level)
    # $log.debug "parseRecordLH at offset #{offset}"
    lhHash = REGISTRY_STRUCT_LH.decode(read_buffer(offset, SIZEOF_REGISTRY_STRUCT_LH))

    if lhHash[:num_keys] > 0
      key_offset = offset + SIZEOF_REGISTRY_STRUCT_LH
      lhHash[:num_keys].times do
        hash = REGISTRY_STRUCT_LH_HASH.decode(read_buffer(key_offset, SIZEOF_REGISTRY_STRUCT_LH_HASH))
        parseRecord hash[:offset_nk], xmlNode, fqName, level
        key_offset += SIZEOF_REGISTRY_STRUCT_LH_HASH
      end
    end
  end

  def parseRecordvk(offset, xmlNode, _fqName, _level)
    # $log.debug "parseRecordVK at offset #{offset}"
    vkHash = REGISTRY_STRUCT_VK.decode(read_buffer(offset, SIZEOF_REGISTRY_STRUCT_VK))
    vkHash[:data_type_display] = KEY_TYPES[vkHash[:data_type]]
    if vkHash[:name_length] == 0
      vkHash[:data_name] = "(Default)"
    else
      vkHash[:data_name] = read_buffer(offset + SIZEOF_REGISTRY_STRUCT_VK, vkHash[:name_length] - 1)
    end

    # Check value filters here
    return if @filter_value && !@filter_value.key?(vkHash[:data_name].downcase)

    begin
      case vkHash[:data_type_display]
      when :REG_SZ, :REG_EXPAND_SZ then vkHash[:data] = getRegString(vkHash, vkHash[:data_type_display])
      when :REG_DWORD    then vkHash[:data] = vkHash[:data_offset]
      when :REG_NONE     then vkHash[:data] = "(zero-length binary value)"
      when :REG_BINARY   then vkHash[:data] = getRegBinary(vkHash)
      when :REG_QWORD    then vkHash[:data] = read_buffer(vkHash[:data_offset], 8).unpack("Q").join.to_i
      when :REG_MULTI_SZ then vkHash[:data] = getRegMultiString(vkHash)
      else
        # Ignore types: REG_RESOURCE_REQUIREMENTS_LIST and REG_RESOURCE_LIS
        if DEBUG_UNHANDLED_DATA
          $log.warn "Unhandled vk record type of [#{vkHash[:data_type]}] [#{vkHash[:data_type_display]}]" unless vkHash[:data_type] == 8 || vkHash[:data_type] == 10 || vkHash[:data_type] >= 12
        end
      end

    ensure
      DumpHash.SortPrint(vkHash, :VK)
      # xmlSubNode = xmlNode
      xmlSubNode = xmlNode.add_element(:value, :type => vkHash[:data_type_display], :name => vkHash[:data_name])
      xmlSubNode.text = vkHash[:data]

      # This is a performance hack right now since searching the whole xml doc for DigitalProductIds takes so long.
      @digitalProductKeys << xmlSubNode if vkHash[:data_name].downcase == "digitalproductid"
    end
  end

  def getRegMultiString(vkHash)
    # $log.debug sprintf("data offset: (0x%X)  Length: [%d]", vkHash['data_offset']+REG_DATA_OFFSET, vkHash['data_length'])
    if vkHash[:data_offset] < 0
      # $log.warn "Invalid offset for multi-string data Key:[#{fqName}] Value:[#{vkHash[:data_name]}] Offset:[#{vkHash[:data_offset]}]"
      return
    end
    vkHash[:data] = read_buffer(vkHash[:data_offset], vkHash[:data_length] - 1)
    vkHash[:data].UnicodeToUtf8!.strip!
  ensure
    vkHash[:data].tr!("\0", "\n") unless vkHash[:data].nil?
  end

  def getRegString(vkHash, key_type)
    # $log.debug sprintf("data offset: (0x%X)  Length: [%d]", vkHash['data_offset']+REG_DATA_OFFSET, vkHash['data_length'])
    if (vkHash[:data_length] & 0x80000000) == 0
      vkHash[:data] = read_buffer(vkHash[:data_offset], vkHash[:data_length] - 1)
      begin
        vkHash[:data].UnicodeToUtf8!
      rescue
        # Since we are getting Unicode strings out of the registry they should be even numbers lengths
        if vkHash[:data_length].remainder(2) == 1
          vkHash[:data] = read_buffer(vkHash[:data_offset], vkHash[:data_length] - 2)
          vkHash[:data].UnicodeToUtf8!
        else
          raise $!
        end
      end
    else
      vkHash[:data] = (vkHash[:data_offset] & 0xFF).chr
    end

    # Truncate string at the first null character
    if i = vkHash[:data].index("\0")
      vkHash[:data] = vkHash[:data][0...i]
    end

    # Resolve expand keys
    @expandEnv.each_pair { |k, v| vkHash[:data].gsub!(k, v) } if key_type == :REG_EXPAND_SZ

    vkHash[:data]
  end

  def getRegBinary(vkHash)
    if (vkHash[:data_length] & 0x80000000) == 0
      res = self.class.rawBinaryToRegBinary(read_buffer(vkHash[:data_offset], vkHash[:data_length] - 1))
    else
      res = vkHash[:data_offset].to_s(16).rjust(8, '0')
      res = "#{res[6..7]},#{res[4..5]},#{res[2..3]},#{res[0..1]}"
    end

    res
  end

  def validateRegFile(fileName)
    t0 = Time.now
    # Do some basic file validation

    fileObj = @fs ? @fs : File
    raise "Registry file [#{fileName}] does not exist." if fileObj.send(@fs ? :fileExists? : :exist?, fileName) == false
    regSize = fileObj.send(@fs ? :fileSize : :size, fileName)
    raise "Registry file [#{fileName}] is empty." if regSize.zero?
    @fileHnd = fileObj.send(@fs ? :fileOpen : :open, fileName, 'rb')
    regf_buf = read_hbin(0)

    raise "Registry file [#{fileName}] does not contain valid marker." if regf_buf[0, 4] != "regf"
    $log.info  "Reading #{fileName} with size (#{regSize})" if DEBUG_PRINT

    # Read in Registry header
    head_string = regf_buf[0, SIZEOF_REGISTRY_HEADER_REGF]
    raise "Registry hive [#{fileName}] does not contain a valid header." unless head_string
    @hiveHash = REGISTRY_HEADER_REGF.decode(head_string)
    @hiveHash[:name].UnicodeToUtf8!.strip!

    # Dump sorted hash results
    DumpHash.SortPrint(@hiveHash, :REGF)

    $log.info "Registry hive [#{File.basename(@hiveHash[:name])}] successfully opened for reading in [#{Time.now - t0}] seconds.  Size:[#{regSize}]  Last registry update: [#{MSRegHive.wtime2time(@hiveHash[:timestamp])}]"
  end

  def typeToString(type)
    case
    when type == 44 then :ROOT
    when type == 32 then :SUB
    when type == 4128 then :SUB
    when type == 16 then :LINK
    else                 :UNKNOWN
    end
  end

  def self.wtime2time(wtime)
    Time.at((wtime - 116444736000000000) / 10000000).getutc
  rescue RangeError
    return nil
  end

  def self.isRegBinary(data)
    data =~ /^[0-9a-fA-F]{2}(,[0-9a-fA-F]{2})*$/
  end

  def self.regBinaryToRawBinary(data)
    raise ArgumentError unless isRegBinary(data)
    [data.delete(',')].pack("H*")
  end

  def self.rawBinaryToRegBinary(data)
    data.unpack("H*")[0].scan(/../).join(',')
  end

  def getHash
    @hiveHash
  end

  def read_buffer(start_offset, data_length)
    # Adjust offset so it matches the length of the actual registry hive file.
    start_offset += REG_DATA_OFFSET

    # Find what hbin section this data is in.  Also loads data from file if it is not already in memory
    idx = load_sections(start_offset / HBIN_SIZE)

    # Subtract the section offset from the full offset to get the position inside the buffer
    @hbin[idx][start_offset - (idx * HBIN_SIZE), data_length + 1]
  end

  def load_sections(idx)
    if @hbin.key?(idx)
      @stats[:cache_hits] += 1 if DEBUG_FILE_READS
      # If the hash points to data return its index.  Otherwise the hash
      # will point to the index of the starting block of data
      return @hbin[idx].kind_of?(Integer) ? @hbin[idx] : idx
    else
      @hbin[idx] = read_hbin(idx)
      binHash = REGISTRY_STRUCT_HBIN.decode(@hbin[idx][0, SIZEOF_REGISTRY_STRUCT_HBIN])

      unless binHash[:id] == 'hbin'
        # If the block does not start with the header sign then back up and find it so
        # we can load the full hbin which spans several block
        while binHash[:id] != 'hbin'
          binHash = REGISTRY_STRUCT_HBIN.decode(read_hbin(idx -= 1)[0, SIZEOF_REGISTRY_STRUCT_HBIN])
        end
      end

      # Determine if the hbin is more than one block
      hbin_count = binHash[:offset_to_next] / HBIN_SIZE
      if hbin_count > 1
        @hbin[idx] = read_hbin(idx, hbin_count)
        # Set contiguous blocks with the index of the starting block
        (idx + 1).upto(idx + hbin_count - 1) { |i| @hbin[i] = idx }
      end
      return idx
    end
  end

  def read_hbin(idx, count = 1)
    startAddr = idx * HBIN_SIZE
    readCount = (HBIN_SIZE * count)
    if DEBUG_FILE_READS
      @stats[:file_reads] += 1
      @stats[:bytes_read] += readCount
    end
    @fileHnd.seek(startAddr, IO::SEEK_SET)
    @fileHnd.read(readCount)
  end

  #  def on_start_element(name, attr_hash)
  #    $log.warn "START KEY: fqName:#{fqName}"
  #  end
  #
  #  def on_end_element(name)
  #    $log.warn "END   KEY: fqName:#{fqName}"
  #  end

  # define registry structures
  REGISTRY_HEADER_REGF = BinaryStruct.new([
    'a4',     :id,                    # ASCII "regf" = 0x66676572
    'i',      :updates1,              # update counter 1
    'i',      :updates2,              # update counter 2
    'Q',      :timestamp,           # last modified (WinNT format)
    'i',      :version_major,       # Version - Major Number
    'i',      :version_minor,       # Version - Minor Number
    'i',      :version_release,     # Version - Release Number
    'i',      :version_build,       # Version - Build Number
    'i',      :data_offset,         # Data offset
    'i',      :last_block,            # Offset of Last Block
    'i',      nil,                    # UNKNOWN for 4 =1
    'a64',    :name,                  # description - last 31 characters of Fully Qualified Hive Name (in Unicode)
    'a396',   nil,                    # UNKNOWN x396
    'i',      :checksum,              # checksum of all DWORDS (XORed) from 0x0000 to 0x01FB
  ])
  SIZEOF_REGISTRY_HEADER_REGF = REGISTRY_HEADER_REGF.size

  REGISTRY_STRUCT_HBIN = BinaryStruct.new([
    'a4',     :id,                    # ASCII "hbin" = 0x6E696268
    'i',      :offset_from_first,   # Offset from 1st hbin-Block
    'i',      :offset_to_next,        # Offset to the next hbin-Block
    'Q',      nil,                    # UNKNOWN for 8
    'Q',      :timestamp,           # last modified (WinNT format)
    'i',      :block_size,            # Block size (including the header!)
    'l',      :length,                # Negative if not used, positive otherwise. Always a multiple of 8
  ])
  SIZEOF_REGISTRY_STRUCT_HBIN = REGISTRY_STRUCT_HBIN.size

  REGISTRY_STRUCT_NK = BinaryStruct.new([
    'a2',     :id,                    # ASCII "nk" = 0x6B6E
    's',      :type,                  # REG_ROOT_KEY = 0x2C, REG_SUB_KEY = 0x20, REG_SYM_LINK = 0x10
    'Q',      :timestamp,
    'a4',     nil,                    # UNKNOWN
    'i',      :parent_offset,       # Offset of Owner/Parent key
    'V',      :num_subkeys,         # Number of Subkeys
    'a4',     nil,                    # UNKNOWN
    'i',      :subkeys_offset,
    'i',      :unknown_offset,
    'i',      :num_values,
    'i',      :values_offset,       # Points to a list of offsets of vk-records
    'i',      :sk_offset,
    'i',      :classname_offset,
    'a20',    nil,                    # UNKNOWN
    's',      :name_length,
    's',      :classname_length,
  ])
  SIZEOF_REGISTRY_STRUCT_NK = REGISTRY_STRUCT_NK.size

  #   # Subkey listing with hash of first 4 characters
  REGISTRY_STRUCT_LH = BinaryStruct.new([
    'a2',     :id,                    # ASCII "lh" = 0x666E
    's',      :num_keys,              # number of keys
  ])
  SIZEOF_REGISTRY_STRUCT_LH = REGISTRY_STRUCT_LH.size

  #   # The vk-record consists information to a single value (value key).
  REGISTRY_STRUCT_VK = BinaryStruct.new([
    'a2',     :id,                    # ASCII "vk" = 0x6B76
    's',      :name_length,
    'i',      :data_length,         # If top-bit set, offset contains the data
    'i',      :data_offset,
    'i',      :data_type,
    's',      :flag,                  # =1, has name, else no name (=Default).
    'a2',     nil,                    # UNKNOWN
  ])
  SIZEOF_REGISTRY_STRUCT_VK = REGISTRY_STRUCT_VK.size

  REGISTRY_STRUCT_LH_HASH = BinaryStruct.new([ #   set STRUCT(REC-LH-HASH) {
    'i',      :offset_nk,           # offset of corresponding NK record
    'a4',     :keyname,             # Key Name
  ])
  SIZEOF_REGISTRY_STRUCT_LH_HASH = REGISTRY_STRUCT_LH_HASH.size

  REGISTRY_STRUCT_VK_OFFSET = BinaryStruct.new([ #   set STRUCT(REC-LH-HASH) {
    'i',      :offset_vk,           # offset of corresponding NK record
  ])
  SIZEOF_REGISTRY_STRUCT_VK_OFFSET = REGISTRY_STRUCT_VK_OFFSET.size

  # The lf-record is the counterpart to the RGKN-record (the hash-function)
  REGISTRY_STRUCT_LF = BinaryStruct.new([
    'a2',     :id,                    # ASCII "lf" = 0x666C
    's',      :num_keys,              # number of keys
  ])
  SIZEOF_REGISTRY_STRUCT_LF = REGISTRY_STRUCT_LF.size

  REGISTRY_STRUCT_LF_HASH = BinaryStruct.new([
    'i',      :offset_nk,           # offset of corresponding NK record
    'a4',     :keyname,             # Key Name
  ])
  SIZEOF_REGISTRY_STRUCT_LF_HASH = REGISTRY_STRUCT_LF_HASH.size

  # A list of offsets to LI/LH records
  REGISTRY_STRUCT_RI = BinaryStruct.new([
    'a2',     :id,                    # ASCII "ri" = 0x6972
    's',      :num_keys,              # number of keys
  ])
  SIZEOF_REGISTRY_STRUCT_RI = REGISTRY_STRUCT_RI.size

  REGISTRY_STRUCT_RI_OFFSET = BinaryStruct.new([ #   set STRUCT(REC-LH-HASH) {
    'i',      :offset_ri,           # offset of corresponding NK record
  ])
  SIZEOF_REGISTRY_STRUCT_RI_OFFSET = REGISTRY_STRUCT_RI_OFFSET.size

  #
  #   # sk (? Security Key ?) is the ACL of the registry.
  #   set STRUCT(REC-SK) {
  #       a2  id                    /* ASCII "sk" = 0x6B73 */
  #       s   tag                   /* */
  #       i   prev_offset           /* Offset of previous "sk"-Record */
  #       i   next_offset           /* Offset of next "sk"-Record */
  #       i   ref_count             /* Reference/Usage counter */
  #       i   rec_size              /* Record size */
  #   }

  # Return registry key type. Otherwise return the hex value of the integer
  KEY_TYPES = Hash.new { |_h, k| "%08X" % k }
  KEY_TYPES.merge!(
    0  => :REG_NONE,              # No value type
    1  => :REG_SZ,                # A null-terminated string (Unicode)
    2  => :REG_EXPAND_SZ,         # A null-terminated string that contains
    #  unexpanded references to environment variables (for example, "%PATH%").
    #  It will be a Unicode or ANSI string depending on whether you use the
    #  Unicode or ANSI functions. To expand the environment variable references,
    #  use the ExpandEnvironmentStrings function.
    3  => :REG_BINARY,            # Free form binary
    4  => :REG_DWORD,             # 32-bit number - Little Endian
    5  => :REG_DWORD_BIG_ENDIAN,  # 32-bit number - Big Endian
    6  => :REG_LINK,              # Symbolic Link (unicode) - Reserved for system use.
    7  => :REG_MULTI_SZ,          # A sequence of null-terminated strings, terminated by an empty string (\0).
    # The following is an example:
    #   String1\0String2\0String3\0LastString\0\0
    # The first \0 terminates the first string, the second to the last \0 terminates the last string,
    # and the final \0 terminates the sequence. Note that the final terminator must be factored into the length of the string.
    8  => :REG_RESOURCE_LIST,     # Resource list in the resource map
    9  => :REG_FULL_RESOURCE_DESCRIPTOR,  # Resource list in the hardware description
    10 => :REG_RESOURCE_REQUIREMENTS_LIST,
    11 => :REG_QWORD,             # 64-bit number - Little Endian
  )
end

module DumpHash
  def self.SortPrint(hash, prefix = :UKN)
    return unless DEBUG_PRINT
    $log.debug "#{prefix}(RAW): ========"
    hash.sort { |a, b| a.to_s <=> b.to_s }.each { |x, y| $log.debug "#{prefix}(#{x})\t\t= #{y}" }
  end
end