ManageIQ/manageiq-smartstate

View on GitHub
lib/metadata/util/win32/peheader.rb

Summary

Maintainability
D
2 days
Test Coverage
C
76%
# encoding: US-ASCII

require 'stringio'
require 'binary_struct'
require 'miq_unicode'

# Notes:
#       The peheader object member 'icons' is an array of icons in the file. Sub 0 is the application
#       icon, 1 is usually the document icon.  The format is the same as an .ico file.  The simple test
#       writes found icons to the root dir as icon0.ico, icon1.ico, etc.  Any icon editor will be able
#       to open them and display each resolution contained in the icon (if more than one).

class PEheader
  using ManageIQ::UnicodeString

  IMAGE_NT_SIGNATURE = "PE\0\0"
  IMAGE_DOS_SIGNATURE = "MZ"

  def initialize(path)
    @fname = path
    @dataDirs = []
    @sectionTable = []
    @baseResDir = nil

    if path.class != String
      @fHnd = path
      fileseek(0, 'init')
      fBuf = fileread(2)
    else
      # Do some basic file validation
      raise Errno::ENOENT, "File [#{@fname}] does not exist." if File.exist?(@fname) == false
      raise "File [#{@fname}] is empty." if File.zero?(@fname)

      # Open file and read contents into buffer
      @fHnd = File.open(@fname, "rb")
      fBuf = fileread(2)
      @fHnd.close
    end

    # Check for the MZ header
    raise "Version Information header not found in file [#{@fname}]" unless fBuf[0..1] == IMAGE_DOS_SIGNATURE

    readPE
  end

  def readPE
    if @fname.class != String
      @fHnd = @fname
      fileseek(0, 'readPE')
    else
      # Open file
      @fHnd = File.open(@fname, "rb")
    end

    # Read contents into buffer
    # TODO: determine the proper amount of data to load here
    @fBuf = fileread(10240)

    # Read offsets for next string
    dhHash = IMAGE_DOS_HEADER.decode(@fBuf)
    offset = dhHash['e_lfanew'] # Offset to PE header
    raise "PE header not found in file [#{@fname}]" unless @fBuf[offset...offset + 4] == IMAGE_NT_SIGNATURE
    offset += 4
    fhHash = IMAGE_FILE_HEADER.decode(@fBuf[offset..-1])
    offset += SIZEOF_IMAGE_FILE_HEADER

    @is64Bit = fhHash['SizeOfOptionalHeader'] == IMAGE_SIZEOF_NT_OPTIONAL64_HEADER
    @IMAGE_OPTIONAL_HEADER = (@is64Bit == true) ? IMAGE_OPTIONAL_HEADER64 : IMAGE_OPTIONAL_HEADER32

    # Commented out the following, since it is not currently being used.
    # ohHash = @IMAGE_OPTIONAL_HEADER.decode(@fBuf[offset, @IMAGE_OPTIONAL_HEADER.size])

    # Read all the data directories & section table.
    offset = getDataDirs(@fBuf, offset)
    offset = getSectionTable(@fBuf, fhHash, offset)
  end

  # These file methods are here to assist in debugging
  def fileseek(offset = 0, _message = nil)
    # st = Time.now
    @fHnd.seek(offset, IO::SEEK_SET)
    # $log.warn "seek time [#{Time.now-st}] from [#{message}]" if $log
  end

  def fileread(length)
    # st = Time.now
    data = @fHnd.read(length)
    # $log.warn "read time [#{Time.now-st}]" if $log
    data
  end

  def imports
    @imports ||= getImports
  end

  def icons
    @icons ||= getIcons(@fBuf)
  end

  def messagetables
    @messagetables ||= getMessagetables
  end

  def versioninfo
    @versioninfo ||= getVersioninfo
  end

  # //////////////////////////////////////////////////////////////////////////
  # //

  def getDataDirs(fBuf, offset)
    offset += @IMAGE_OPTIONAL_HEADER.size
    IMAGE_NUMBEROF_DIRECTORY_ENTRIES.times do
      ddHash = IMAGE_DATA_DIRECTORY.decode(fBuf[offset..-1])
      offset += SIZEOF_IMAGE_DATA_DIRECTORY
      @dataDirs.push(ddHash)
    end
    offset
  end

  def getSectionTable(fBuf, fhHash, offset)
    fhHash['NumberOfSections'].times do
      shHash = IMAGE_SECTION_HEADER.decode(fBuf[offset..-1])
      offset += SIZEOF_IMAGE_SECTION_HEADER
      @sectionTable.push(shHash)
    end
    offset
  end

  def getImports
    imports_libs = []
    import = @dataDirs[IMAGE_DIRECTORY_ENTRY_IMPORT]
    import[:offset] = import[:virtualAddress]
    if import[:offset] != 0
      import[:offset] = adjustAddress(import[:offset])
      fileseek(import[:offset], 'getImports')
      data = fileread(import[:size])
      offset = 0
      loop do
        iiHash = IMAGE_IMPORT_DESCRIPTOR.decode(data[offset..-1])
        break if iiHash['Name'] == 0
        offset += SIZEOF_IMAGE_IMPORT_DESCRIPTOR
        iiHash['Name'] = adjustAddress(iiHash['Name']) - import[:offset]

        # Check if we have enough data.  This happens if the import data only contains pointers
        if (data.length <= iiHash['Name'])
          size = iiHash['Name'] - data.length + 4096
          data += fileread(size)
        end

        nameEnd = iiHash['Name'] + data[iiHash['Name']..-1].index("\0") - 1
        imports_libs.push(data[iiHash['Name']..nameEnd].downcase)
      end
    end
    imports_libs
  end

  def getIcons(fBuf)
    iconEntries = getRawIcons(fBuf)
    grpIcons = getIconDirEntries(fBuf)
    assembleIcons(iconEntries, grpIcons)
  end

  def getRawIcons(_fBuf)
    # Read raw icons.
    iconEntries = []
    get_resources_by_type(RT_ICON) do |icon_rsc|
      ent = icon_rsc[:data]
      ent[:offset] = adjustAddress(ent[:offsetToData])
      fileseek(ent[:offset], 'getRawIcons')
      icon_rsc[:icon] = fileread(ent[:size])
      iconEntries << icon_rsc
    end
    iconEntries
  end

  def getIconDirEntries(_fBuf)
    # Read icon directory.
    grpIcons = []
    # iconDirEntries = getDataEntries(RT_GROUP_ICON, fBuf)
    get_resources_by_type(RT_GROUP_ICON) do |icon_rsc|
      ent = icon_rsc[:data]
      ent[:offset] = adjustAddress(ent[:offsetToData])
      fileseek(ent[:offset], 'getIconDirEntries')
      iconDir = fileread(ent[:size])
      iconDir = GRPICONDIR.decode(iconDir)
      grpIconDirEntries = []
      0.upto(iconDir[:idCount] - 1) { |i| grpIconDirEntries << GRPICONDIRENTRY.decode(iconDir[:data][i * SIZEOF_GRPICONDIRENTRY, SIZEOF_GRPICONDIRENTRY]) }
      grpIcons << grpIconDirEntries
    end
    grpIcons
  end

  def assembleIcons(iconEntries, grpIcons)
    # For each major sub in grpIcons, construct an .ico blob.
    icons = []
    0.upto(grpIcons.size - 1) do |fileIdx|
      # Write icon directory.
      baseOffset = 16 * grpIcons[fileIdx].size + 6
      thisOffset = 0
      ico = StringIO.new
      ico.write([0].pack('S'))                      # idReserved
      ico.write([1].pack('S'))                      # idType
      ico.write([grpIcons[fileIdx].size].pack('S')) # idCount
      0.upto(grpIcons[fileIdx].size - 1) do |iconIdx|
        icon = grpIcons[fileIdx][iconIdx]
        # Write icon dir entry.
        ico.write([icon[:bWidth]].pack('C'))
        ico.write([icon[:bHeight]].pack('C'))
        ico.write([icon[:bColorCount]].pack('C'))
        ico.write([0].pack('C'))
        ico.write([icon[:wPlanes]].pack('S'))
        ico.write([icon[:wBitCount]].pack('S'))
        ico.write([icon[:dwBytesInRes]].pack('L'))
        ico.write([baseOffset + thisOffset].pack('L'))
        thisOffset += icon[:dwBytesInRes]
      end
      # Write icon data.
      0.upto(grpIcons[fileIdx].size - 1) { |iconIdx| ico.write(getIconById(iconEntries, grpIcons[fileIdx][iconIdx][:nID])) }
      # Save it as a string.
      ico.rewind
      icons << ico.read
    end
    icons
  end

  # Find a particular raw icon.
  def getIconById(icons, id)
    icons.each { |icon| return icon[:icon] if icon[:rsc_id] == id }
    nil
  end

  def getMessagetables(requested_locale = 0x0409)
    # Read message table resources.
    messagetables = {}
    get_resources_by_type(RT_MESSAGETABLE, requested_locale) do |msg_resource|
      # Get the block directory for this messagetable.
      msg_data = msg_resource[:data]
      offset = adjustAddress(msg_data[:offsetToData]) - @dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:offset]
      blkdir = MESSAGE_RESOURCE_DATA.decode(@dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:data][offset, msg_data[:size]])
      0.upto(blkdir[:numberOfBlocks] - 1) do |i|
        # Break out each block.
        blk = MESSAGE_RESOURCE_BLOCK.decode(blkdir[:data][i * SIZEOF_MRB, SIZEOF_MRB])
        adrs = blk[:offsetToEntries] - 4

        # Grab the block's strings.
        blk[:loId].upto(blk[:hiId]) do |idx|
          ent1 = MESSAGE_RESOURCE_ENTRY.decode(blkdir[:data][adrs, SIZEOF_MRE])
          if ent1[:length] > 0
            len = ent1[:length] - SIZEOF_MRE
            str = blkdir[:data][adrs + SIZEOF_MRE, len]
            (ent1[:flags] == MESSAGE_RESOURCE_UNICODE) ? str.UnicodeToUtf8! : str.AsciiToUtf8!
            str.gsub!(/\000/, "")
            messagetables[idx] = str
            adrs += len
          end
          adrs += SIZEOF_MRE
        end
      end
    end
    messagetables
  end

  # Get versioninfo resource.
  def getVersioninfo(requested_locale = 0x0409)
    aVersioninfoHash = {}
    get_resources_by_type(RT_VERSION, requested_locale) do |versionEntry|
      ent = versionEntry[:data]
      offset = adjustAddress(ent[:offsetToData]) - @dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:offset]
      versionInfo = @dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:data][offset, ent[:size]]
      # versionInfo is a VS_FIXEDFILEINFO structure followed by StringFileInfo.
      aVersioninfoHash = getVersionInfoHash(versionInfo)
    end
    aVersioninfoHash
  end

  # Walk the resource directories and collect all the directories and resource pointers
  def getDataEntries(fBuf, rsc_id = nil)
    result = {}
    getBaseResDir(fBuf) { |baseResDir| dumpResourceDirectory(baseResDir, 0, result, rsc_id) }
    result
  end

  def dumpResourceDirectory(resDir, level, data_hash, rsc_id = nil)
    resDirEntry = getResourceDirectoryEntry(resDir)

    # Process each entry in the directory.
    # Note: Named entries are listed first.
    1.upto(resDir[:numberOfNamedEntries]) do
      dumpResourceEntry(resDirEntry, level + 1, data_hash, rsc_id)
      resDirEntry = getNextResourceDirectoryEntry(resDirEntry)
    end

    1.upto(resDir[:numberOfIdEntries]) do
      dumpResourceEntry(resDirEntry, level + 1, data_hash, rsc_id)
      resDirEntry = getNextResourceDirectoryEntry(resDirEntry)
    end
  end

  def dumpResourceEntry(resDirEntry, level, data_hash, rsc_id)
    # 1.upto(level) {print "  "}

    resDirEntry[:name] = getResourceDirectoryEntryName(resDirEntry)
    resDirEntry[:level] = level
    if resDirEntry[:isDir]

      # Filter by resource type so we do not process every available resource
      return if level == 1 && rsc_id && resDirEntry[:name] != rsc_id

      resDir = getResourceDirectory(resDirEntry)
      resDirEntry[:numberOfIdEntries] = resDir[:numberOfIdEntries]
      resDirEntry[:numberOfNamedEntries] = resDir[:numberOfNamedEntries]
      # puts "DIR: #{resDirEntry.inspect}"

      data_hash[resDirEntry[:name]] = resDirEntry
      resDirEntry[:children] = {}

      dumpResourceDirectory(resDir, level, resDirEntry[:children], rsc_id)
    else
      data_hash[resDirEntry[:name]] = resDirEntry
      resDirEntry[:data] = IMAGE_RESOURCE_DATA_ENTRY.decode(@dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:data][resDirEntry[:offsetToData]..-1])
      # puts "RSC: #{resDirEntry.inspect}"
    end
  end

  def getResourceDirectory(resDirEntry)
    offset = resDirEntry[:offsetToData] & 0x7fffffff
    resDir = IMAGE_RESOURCE_DIRECTORY.decode(@dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:data][offset..-1])
    resDir[:offset_into_data] = offset
    resDir
  end

  def getResourceDirectoryEntry(resDir)
    getNextResourceResourceEntry(resDir, IMAGE_RESOURCE_DIRECTORY_ENTRY, SIZEOF_IMAGE_RESOURCE_DIRECTORY)
  end

  def getNextResourceDirectoryEntry(resDirEntry)
    getNextResourceResourceEntry(resDirEntry, IMAGE_RESOURCE_DIRECTORY_ENTRY, SIZEOF_IMAGE_RESOURCE_DIRECTORY_ENTRY)
  end

  def getNextResourceResourceEntry(resEntry, rsc_type, size)
    offset = resEntry[:offset_into_data] + size
    entry = rsc_type.decode(@dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:data][offset..-1])
    entry[:offset_into_data] = offset
    entry[:isDir] = bit?(entry[:offsetToData], 31)
    entry
  end

  def getBaseResDir(_fBuf)
    if @baseResDir.nil?
      rsc = @dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE]
      rsc[:offset] = rsc[:virtualAddress]
      unless rsc[:offset].zero?
        rsc[:offset] = adjustAddress(rsc[:offset])
        # Read in the resource part the file
        fileseek(rsc[:offset], 'getBaseResDir')
        rsc[:data] = fileread(rsc[:size])
        @baseResDir = IMAGE_RESOURCE_DIRECTORY.decode(rsc[:data][0, SIZEOF_IMAGE_RESOURCE_DIRECTORY])
        @baseResDir[:offset_into_data] = 0
      end
    end
    yield(@baseResDir) unless @baseResDir.nil?
  end

  def get_resources_by_type(rt, locale_id = nil)
    if (rsc = getDataEntries(@fBuf, rt)[rt])
      resources = []
      find_all_resources(rsc[:children]) { |r| resources << r }
      return if resources.empty?

      # Finding a resource is often by locale.  If we do not find the requested
      # locale then return the first one.
      unless locale_id.nil?
        local_rsc = resources.detect { |r| r[:lang_id] == locale_id }
        resources = local_rsc.nil? ? [resources.first] : [local_rsc]
      end

      # Yield the resource data to the caller
      resources.each { |r| yield(r) }
    end
  end

  def find_all_resources(rsc, rsc_id = nil, &blk)
    # Resource Directory Levels:
    # 1 = Resource Type
    # 2 = Resource Identifier
    # 3 = Resource Langauge ID
    rsc.each do |lang_id, item|
      if item[:isDir]
        rsc_id = item[:name] if item[:level] == 2
        find_all_resources(item[:children], rsc_id, &blk)
      else
        item[:rsc_id]  = rsc_id
        item[:lang_id] = lang_id
        yield(item)
      end
    end
  end

  def getResourceDirectoryEntryName(resDirEntry)
    return resDirEntry[:name] unless bit?(resDirEntry[:name], 31)

    # The low 30 bits of the 'Name' member is an offset to an IMAGE_RESOURCE_DIRECTORY_STRING_U struct.
    str = ""
    ptr = (resDirEntry[:name] & 0x7fffffff)
    len = @dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:data][ptr, 2].unpack('S')[0]
    str = @dataDirs[IMAGE_DIRECTORY_ENTRY_RESOURCE][:data][ptr + 2, len * 2]
    str.UnicodeToUtf8!
  end

  def adjustAddress(rva)
    @sectionTable.each do |s|
      # Is the RVA within this section?
      if  (rva >= s[:virtualAddress]) && (rva < (s[:virtualAddress] + s[:VirtualSize]))
        delta = s[:virtualAddress] - s[:PointerToRawData]
        return rva - delta
      end
    end
    nil
  end

  def getImportList
    return nil if imports.nil?
    unless imports.empty?
      import_list = ""
      imports.each { |i| import_list += i + ", " }
      return import_list.rstrip.chomp(",")
    end
  end

  def getVersionInfoHash(fBuf)
    viHash = {}

    # Find VS Version Info signature
    idx = fBuf.index(VS_VERSION_INFO)
    return viHash unless idx
    # raise "Version Information header not found in file" unless idx
    # $log.debug sprintf("Found at index: [0X%08X] (%d)", idx, idx)

    # Reduce buffer to just the signature to the end of the file
    fBuf = fBuf[idx..fBuf.length]
    offset = 0
    vhHash = VS_VERSION_INFO_HEADER.decode(fBuf[offset..(offset + SIZEOF_VS_VERSION_INFO_HEADER)])

    # Create VersionInfo hash
    viHash['FILEVERSION_HEADER']    = vhHash['fmajor'].to_s + "," + vhHash['fminor'].to_s + "," + vhHash['frev'].to_s + "," + vhHash['fbuild'].to_s
    viHash['PRODUCTVERSION_HEADER'] = vhHash['pmajor'].to_s + "," + vhHash['pminor'].to_s + "," + vhHash['prev'].to_s + "," + vhHash['pbuild'].to_s

    # Find the string file info signautre
    idx = fBuf.index(STRINGFILEINFO)
    return viHash unless idx
    # raise "String File information header not found in file [#{fname}]" unless idx

    offset = idx
    viEnd = offset + SIZEOF_STRING_INFO_HEADER
    viHash.merge!(STRING_INFO_HEADER.decode(fBuf[offset..viEnd]))
    viHash['sig'].UnicodeToUtf8!.tr!("\0", "")
    viHash['code_page'].UnicodeToUtf8!.tr!("\0", "")
    viHash['lang'].UnicodeToUtf8!.tr!("\0", "")

    # Read offsets for next string
    offset = viEnd
    vsHash = VERSION_STRING_HEADER.decode(fBuf[offset..offset + 6])

    # Calculate the amount of version info data
    offset_end = offset + viHash['data_length'] - SIZEOF_STRING_INFO_HEADER

    while offset < offset_end
      break unless vsHash['zero'] == 0
      break if vsHash['zero'].nil? || vsHash['vlen'].nil? || vsHash['slen'].nil?
      offset += SIZEOF_VERSION_STRING_HEADER
      name_len = vsHash['slen'] - 4 - (vsHash['vlen'] * 2) - 2
      name = fBuf[offset...offset + name_len]
      offset += name_len
      value_len = (vsHash['vlen'] * 2) - 2
      value = fBuf[offset...offset + value_len]
      break if name.nil? or value.nil? or name.empty?
      name.UnicodeToUtf8!.delete!("\0")
      # Do not allow spaces in the attribute names (will invalidate a XML file)
      name.tr!(" ", "_")
      value.UnicodeToUtf8!.delete!("\0")
      # $log.debug "[#{name}] => [#{value}]"
      viHash[name] = value
      offset += value_len + (vsHash['vlen'] % 2 * 2)

      # Read next offset header
      vsHash = VERSION_STRING_HEADER.decode(fBuf[offset..offset + 6])

      # This is a work-around.  In case the offset to the next record is slightly off
      unless vsHash['zero'] == 0
        offset -= 2
        # Read next offset header
        vsHash = VERSION_STRING_HEADER.decode(fBuf[offset..offset + 6])
      end
    end

    viHash
  end

  ################################################################
  #  PE Header structures defined
  ################################################################
  # From WINNT.H
  #
  # // Directory Entries
  #
  # // Export Directory
  IMAGE_DIRECTORY_ENTRY_EXPORT      = 0
  # // Import Directory
  IMAGE_DIRECTORY_ENTRY_IMPORT      = 1
  # // Resource Directory
  IMAGE_DIRECTORY_ENTRY_RESOURCE    = 2
  # // Exception Directory
  IMAGE_DIRECTORY_ENTRY_EXCEPTION   = 3
  # // Security Directory
  IMAGE_DIRECTORY_ENTRY_SECURITY    = 4
  # // Base Relocation Table
  IMAGE_DIRECTORY_ENTRY_BASERELOC   = 5
  # // Debug Directory
  IMAGE_DIRECTORY_ENTRY_DEBUG       = 6
  # // Description String
  IMAGE_DIRECTORY_ENTRY_COPYRIGHT   = 7
  # // Machine Value (MIPS GP)
  IMAGE_DIRECTORY_ENTRY_GLOBALPTR   = 8
  # // TLS Directory
  IMAGE_DIRECTORY_ENTRY_TLS         = 9
  # // Load Configuration Directory
  IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10

  IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16

  # From WinUser.h
  # /*
  # * Predefined Resource Types
  # */
  RT_CURSOR       = 1
  RT_BITMAP       = 2
  RT_ICON         = 3
  RT_MENU         = 4
  RT_DIALOG       = 5
  RT_STRING       = 6
  RT_FONTDIR      = 7
  RT_FONT         = 8
  RT_ACCELERATOR  = 9
  RT_RCDATA       = 10
  RT_MESSAGETABLE = 11
  RT_GROUP_ICON   = 14
  RT_VERSION      = 16

  IMAGE_FILE_HEADER = BinaryStruct.new([
    'v',   'Machine',
    'v',   'NumberOfSections',
    'V',   'TimeDateStamp',
    'V',   'PointerToSymbolTable',
    'V',   'NumberOfSymbols',
    'v',   'SizeOfOptionalHeader',
    'v',   'Characteristics',
  ])
  SIZEOF_IMAGE_FILE_HEADER = IMAGE_FILE_HEADER.size

  IMAGE_DOS_HEADER = BinaryStruct.new([       # // DOS .EXE header
    'v',   'e_magic',                     # // Magic number
    'v',   'e_cblp',                      # // Bytes on last page of file
    'v',   'e_cp',                        # // Pages in file
    'v',   'e_crlc',                      # // Relocations
    'v',   'e_cparhdr',                   # // Size of header in paragraphs
    'v',   'e_minalloc',                  # // Minimum extra paragraphs needed
    'v',   'e_maxalloc',                  # // Maximum extra paragraphs needed
    'v',   'e_ss',                        # // Initial (relative) SS value
    'v',   'e_sp',                        # // Initial SP value
    'v',   'e_csum',                      # // Checksum
    'v',   'e_ip',                        # // Initial IP value
    'v',   'e_cs',                        # // Initial (relative) CS value
    'v',   'e_lfarlc',                    # // File address of relocation table
    'v',   'e_ovno',                      # // Overlay number
    'v',   nil,                           # // Reserved words - e_res[4]
    'v',   nil,                           # // Reserved words - e_res[4]
    'v',   nil,                           # // Reserved words - e_res[4]
    'v',   nil,                           # // Reserved words - e_res[4]
    'v',   'e_oemid',                     # // OEM identifier (for e_oeminfo)
    'v',   'e_oeminfo',                   # // OEM information; e_oemid specific
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'v',   nil,                           # // Reserved words - e_res2[10]
    'V',   'e_lfanew',                    # // File address of new exe header
  ])
  SIZEOF_IMAGE_DOS_HEADER = IMAGE_DOS_HEADER.size

  IMAGE_OPTIONAL_HEADER32 = BinaryStruct.new([
    #    //
    #    // Standard fields.
    #    //

    'v',    'Magic',
    'c',    'MajorLinkerVersion',
    'c',    'MinorLinkerVersion',
    'V',    'SizeOfCode',
    'V',    'SizeOfInitializedData',
    'V',    'SizeOfUninitializedData',
    'V',    'AddressOfEntryPoint',
    'V',    'BaseOfCode',
    'V',    'BaseOfData',
    #
    #    //
    #    // NT additional fields.
    #    //
    #
    'V',    'ImageBase',
    'V',    'SectionAlignment',
    'V',    'FileAlignment',
    'v',    'MajorOperatingSystemVersion',
    'v',    'MinorOperatingSystemVersion',
    'v',    'MajorImageVersion',
    'v',    'MinorImageVersion',
    'v',    'MajorSubsystemVersion',
    'v',    'MinorSubsystemVersion',
    'V',    'Win32VersionValue',
    'V',    'SizeOfImage',
    'V',    'SizeOfHeaders',
    'V',    'CheckSum',
    'v',    'Subsystem',
    'v',    'DllCharacteristics',
    'V',    'SizeOfStackReserve',
    'V',    'SizeOfStackCommit',
    'V',    'SizeOfHeapReserve',
    'V',    'SizeOfHeapCommit',
    'V',    'LoaderFlags',
    'V',    'NumberOfRvaAndSizes',
  ])
  SIZEOF_IMAGE_OPTIONAL_HEADER32 = IMAGE_OPTIONAL_HEADER32.size

  IMAGE_OPTIONAL_HEADER64 = BinaryStruct.new([
    'v',    'Magic',
    'c',    'MajorLinkerVersion',
    'c',    'MinorLinkerVersion',
    'V',    'SizeOfCode',
    'V',    'SizeOfInitializedData',
    'V',    'SizeOfUninitializedData',
    'V',    'AddressOfEntryPoint',
    'V',    'BaseOfCode',
    'Q',    'ImageBase',
    'V',    'SectionAlignment',
    'V',    'FileAlignment',
    'v',    'MajorOperatingSystemVersion',
    'v',    'MinorOperatingSystemVersion',
    'v',    'MajorImageVersion',
    'v',    'MinorImageVersion',
    'v',    'MajorSubsystemVersion',
    'v',    'MinorSubsystemVersion',
    'V',    'Win32VersionValue',
    'V',    'SizeOfImage',
    'V',    'SizeOfHeaders',
    'V',    'CheckSum',
    'v',    'Subsystem',
    'v',    'DllCharacteristics',
    'Q',    'SizeOfStackReserve',
    'Q',    'SizeOfStackCommit',
    'Q',    'SizeOfHeapReserve',
    'Q',    'SizeOfHeapCommit',
    'V',    'LoaderFlags',
    'V',    'NumberOfRvaAndSizes',
  ])
  SIZEOF_IMAGE_OPTIONAL_HEADER64 = IMAGE_OPTIONAL_HEADER64.size

  IMAGE_DATA_DIRECTORY = BinaryStruct.new([
    'V',   :virtualAddress,
    'V',   :size,
  ])
  SIZEOF_IMAGE_DATA_DIRECTORY = IMAGE_DATA_DIRECTORY.size

  IMAGE_SECTION_HEADER = BinaryStruct.new([
    'a8',  'Name',
    #    union {
    #            DWORD   PhysicalAddress;
    #            DWORD   VirtualSize;
    #    } Misc;
    'V',   :VirtualSize,
    'V',   :virtualAddress,
    'V',   'SizeOfRawData',
    'V',   :PointerToRawData,
    'V',   'PointerToRelocations',
    'V',   'PointerToLinenumbers',
    'v',   'NumberOfRelocations',
    'v',   'NumberOfLinenumbers',
    'V',   'Characteristics',
  ])
  SIZEOF_IMAGE_SECTION_HEADER = IMAGE_SECTION_HEADER.size

  IMAGE_IMPORT_DESCRIPTOR = BinaryStruct.new([
    #    union {
    #        DWORD   Characteristics;           #// 0 for terminating null import descriptor
    #        DWORD   OriginalFirstThunk;        #// RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    #    };
    'V',  'Characteristics',
    'V',  'TimeDateStamp',                  # // 0 if not bound,
    # // -1 if bound, and real date\time stamp
    # //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
    # // O.W. date/time stamp of DLL bound to (Old BIND)

    'V',   'ForwarderChain',                # // -1 if no forwarders
    'V',   'Name',
    'V',   'FirstThunk',                    # // RVA to IAT (if bound this IAT has actual addresses)
  ])
  SIZEOF_IMAGE_IMPORT_DESCRIPTOR = IMAGE_IMPORT_DESCRIPTOR.size

  # General resource definitions.
  IMAGE_RESOURCE_DIRECTORY = BinaryStruct.new([
    'L',  :characteristics,
    'L',  :timeDateStamp,
    'S',  :majorVersion,
    'S',  :minorVersion,
    'S',  :numberOfNamedEntries,              # Number of named entries that follow this struc (first).
    'S',  :numberOfIdEntries,               # Number of ID entries that follow this struc (second).
  ])
  SIZEOF_IMAGE_RESOURCE_DIRECTORY = IMAGE_RESOURCE_DIRECTORY.size

  IMAGE_RESOURCE_DIRECTORY_ENTRY = BinaryStruct.new([
    'L',  :name,                              # Name or ID. If bit 31 = 0 then ID. If bit 31 = 1, then
    # bits 0-30 are an offset (from start of rsrc) to IMAGE_RESOURCE_DIR_STRING_U.
    'L',  :offsetToData,                      # Ptr to dir or data. If bit 31 = 0, then ptr to
    # IMAGE_REDSOURCE_DATA_ENTRY. If bit 31 = 1, then bits 0-30 are ptr to IMAGE_RESOURCE_DIRECTORY.
  ])
  SIZEOF_IMAGE_RESOURCE_DIRECTORY_ENTRY = IMAGE_RESOURCE_DIRECTORY_ENTRY.size

  # NOTE: Skipping string resource name because it is self-referencing:
  # typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
  #    WORD    Length;
  #    WCHAR   NameString[ 1 ];
  # } IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
  # The member NameString is Length characters long, so the final size of the structure is unknown.
  # This is just handled without BinaryStruct.

  IMAGE_RESOURCE_DATA_ENTRY = BinaryStruct.new([
    'L',  :offsetToData,                      # This offset is an RVA.
    'L',  :size,                              # Size in bytes.
    'L',  :codePage,                          # Code page (for strings).
    'L',  :reserved1,
  ])
  SIZEOF_IMAGE_RESOURCE_DATA_ENTRY = IMAGE_RESOURCE_DATA_ENTRY.size

  # Icon specific resource definitions.
  GRPICONDIR = BinaryStruct.new([
    'S',  :idReserved1,
    'S',  :idType,                            # 1 for icons.
    'S',  :idCount,                           # Count of images.
    'a*', :data,                              # Array of GRPICONDIRENTRY.
  ])
  SIZEOF_GRPICONDIR = GRPICONDIR.size           # TODO: BinaryStruct.sizeof ignores the *

  GRPICONDIRENTRY = BinaryStruct.new([
    'C',  :bWidth,                            # Pixel width of image.
    'C',  :bHeight,                           # Pixel height of image.
    'C',  :bColorCount,                       # Colors in image (0 if >= 8bpp).
    'C',  :bReserved1,
    'S',  :wPlanes,                           # Color planes.
    'S',  :wBitCount,                         # Bits per pixel.
    'L',  :dwBytesInRes,                      # Bytes in this resource.
    'S',  :nID,                               # Resource ID.
    # NOTE: In an .ico file, last member is 'L', 'dwImageOffset', an offset
    # from the beginning of the file to the BITMAPINFOHEADER of the icon data.
  ])
  SIZEOF_GRPICONDIRENTRY = GRPICONDIRENTRY.size

  # Messagetable specific resource definitions.
  MESSAGE_RESOURCE_DATA = BinaryStruct.new([
    'L',  :numberOfBlocks,                    # Length of data array.
    'a*', :data,                              # Array of MESSAGE_RESOURCE_BLOCK.
  ])
  SIZEOF_MESSAGE_RESOURCE_DATA = MESSAGE_RESOURCE_DATA.size    # TODO: BinaryStruct.sizeof ignores the *

  MESSAGE_RESOURCE_BLOCK = BinaryStruct.new([
    'L',  :loId,
    'L',  :hiId,
    'L',  :offsetToEntries,                 # RVA?
  ])
  SIZEOF_MESSAGE_RESOURCE_BLOCK = MESSAGE_RESOURCE_BLOCK.size
  SIZEOF_MRB = 12

  MESSAGE_RESOURCE_ENTRY = BinaryStruct.new([
    'S',  :length,                            # String length.
    'S',  :flags,                           # Encoding (see below).
  ])
  SIZEOF_MRE = 4
  SIZEOF_MESSAGE_RESOURCE_ENTRY = MESSAGE_RESOURCE_ENTRY.size
  # Text follows here.

  MESSAGE_RESOURCE_ANSI     = 0x0000          # If set text is ANSI.
  MESSAGE_RESOURCE_UNICODE  = 0x0001          # If set text is UNICODE.

  VS_VERSION_INFO_HEADER = BinaryStruct.new([
    'a32', 'sig',
    's',   nil,
    's',   nil,
    's',   nil,
    's',   nil,
    's',   nil,
    'S',   'fminor',
    'S',   'fmajor',
    'S',   'fbuild',
    'S',   'frev',
    'S',   'pminor',
    'S',   'pmajor',
    'S',   'pbuild',
    'S',   'prev',
  ])
  SIZEOF_VS_VERSION_INFO_HEADER = VS_VERSION_INFO_HEADER.size

  STRING_INFO_HEADER = BinaryStruct.new([
    'a30', 'sig',
    'V',   'data_length',
    's',   'type',
    'a8',  'lang',
    'a8',  'code_page',
  ])
  SIZEOF_STRING_INFO_HEADER = STRING_INFO_HEADER.size

  VERSION_STRING_HEADER = BinaryStruct.new([
    's', 'zero',
    's', 'slen',
    's', 'vlen',
    's', 'type',
  ])
  SIZEOF_VERSION_STRING_HEADER = VERSION_STRING_HEADER.size

  STRINGFILEINFO  = "S\0t\0r\0i\0n\0g\0F\0i\0l\0e\0I\0n\0f\0o\0\0\0"
  VS_VERSION_INFO = "V\0S\0_\0V\0E\0R\0S\0I\0O\0N\0_\0I\0N\0F\0O\0\0\0"

  IMAGE_SIZEOF_NT_OPTIONAL32_HEADER = 224
  IMAGE_SIZEOF_NT_OPTIONAL64_HEADER = 240

  IMAGE_NT_OPTIONAL_HDR32_MAGIC     = 0x10b
  IMAGE_NT_OPTIONAL_HDR64_MAGIC     = 0x20b

  private

  def bit?(num, bitNum)
    msk = 1 << bitNum
    num & msk == msk
  end
end

###########################################################
# Only run if we are calling this script directly
if __FILE__ == $0
  st = Time.now
  puts "Running script [#{__FILE__}]"
  fileName = "D:/temp/icons/PSPad.exe"
  fileName = "D:/temp/icons/EventMsg2.dll"
  peHdr = PEheader.new(fileName)
  puts "Imports:[#{peHdr.imports.length}] - #{peHdr.imports.join(", ")}"
  puts "VerionsInfo: #{peHdr.versioninfo.inspect}"
  puts "Icon Count: [#{peHdr.icons.length}]"
  # Dump icons to d:\temp\icons\icon{n}.ico
  peHdr.icons.each_with_index { |icon, ico| File.open("d:/temp/icons/icon#{ico}.ico", "wb") { |f| f.write(icon) } }

  puts "MessageTable Count: [#{peHdr.messagetables.length}]"
  peHdr.messagetables.each { |m| puts m }

  puts "completed script [#{__FILE__}]  [#{Time.now - st}]"
end