Windows resource file: Ruby parsing library

Windows resource file (.res) are binary bundles of "resources". Resource has some sort of ID (numerical or string), type (predefined or user-defined), and raw value. Resource files can be seen standalone (as .res file), or embedded inside PE executable (.exe, .dll) files.

Typical use cases include:

  • providing information about the application (such as title, copyrights, etc)
  • embedding icon(s) to be displayed in file managers into .exe
  • adding non-code data into the binary, such as menus, dialog forms, cursor images, fonts, various misc bitmaps, and locale-aware strings

Windows provides special API to access "resources" from a binary.

Normally, resources files are created with rc compiler: it takes a .rc file (so called "resource-definition script") + all the raw resource binary files for input, and outputs .res file. That .res file can be linked into an .exe / .dll afterwards using a linker.

Internally, resource file is just a sequence of individual resource definitions. RC tool ensures that first resource (#0) is always empty.

File extension

res

KS implementation details

License: CC0-1.0

References

This page hosts a formal specification of Windows resource file using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Runtime library

All parsing code for Ruby generated by Kaitai Struct depends on the Ruby runtime library. You have to install it before you can parse data.

The Ruby runtime library can be installed from RubyGems:

gem install kaitai-struct

Code

Parse a local file and get structure in memory:

data = WindowsResourceFile.from_file("path/to/local/file.res")

Or parse structure from a string of bytes:

bytes = "\x00\x01\x02..."
data = WindowsResourceFile.new(Kaitai::Struct::Stream.new(bytes))

After that, one can get various attributes from the structure by invoking getter methods like:

data.resources # => get resources

Ruby source code to parse Windows resource file

windows_resource_file.rb

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

require 'kaitai/struct/struct'

unless Gem::Version.new(Kaitai::Struct::VERSION) >= Gem::Version.new('0.9')
  raise "Incompatible Kaitai Struct Ruby API: 0.9 or later is required, but you have #{Kaitai::Struct::VERSION}"
end


##
# Windows resource file (.res) are binary bundles of
# "resources". Resource has some sort of ID (numerical or string),
# type (predefined or user-defined), and raw value. Resource files can
# be seen standalone (as .res file), or embedded inside PE executable
# (.exe, .dll) files.
# 
# Typical use cases include:
# 
# * providing information about the application (such as title, copyrights, etc)
# * embedding icon(s) to be displayed in file managers into .exe
# * adding non-code data into the binary, such as menus, dialog forms,
#   cursor images, fonts, various misc bitmaps, and locale-aware
#   strings
# 
# Windows provides special API to access "resources" from a binary.
# 
# Normally, resources files are created with `rc` compiler: it takes a
# .rc file (so called "resource-definition script") + all the raw
# resource binary files for input, and outputs .res file. That .res
# file can be linked into an .exe / .dll afterwards using a linker.
# 
# Internally, resource file is just a sequence of individual resource
# definitions. RC tool ensures that first resource (#0) is always
# empty.
class WindowsResourceFile < Kaitai::Struct::Struct
  def initialize(_io, _parent = nil, _root = self)
    super(_io, _parent, _root)
    _read
  end

  def _read
    @resources = []
    i = 0
    while not @_io.eof?
      @resources << Resource.new(@_io, self, @_root)
      i += 1
    end
    self
  end

  ##
  # Each resource has a `type` and a `name`, which can be used to
  # identify it, and a `value`. Both `type` and `name` can be a
  # number or a string.
  # @see https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader Source
  class Resource < Kaitai::Struct::Struct

    PREDEF_TYPES = {
      1 => :predef_types_cursor,
      2 => :predef_types_bitmap,
      3 => :predef_types_icon,
      4 => :predef_types_menu,
      5 => :predef_types_dialog,
      6 => :predef_types_string,
      7 => :predef_types_fontdir,
      8 => :predef_types_font,
      9 => :predef_types_accelerator,
      10 => :predef_types_rcdata,
      11 => :predef_types_messagetable,
      12 => :predef_types_group_cursor,
      14 => :predef_types_group_icon,
      16 => :predef_types_version,
      17 => :predef_types_dlginclude,
      19 => :predef_types_plugplay,
      20 => :predef_types_vxd,
      21 => :predef_types_anicursor,
      22 => :predef_types_aniicon,
      23 => :predef_types_html,
      24 => :predef_types_manifest,
    }
    I__PREDEF_TYPES = PREDEF_TYPES.invert
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @value_size = @_io.read_u4le
      @header_size = @_io.read_u4le
      @type = UnicodeOrId.new(@_io, self, @_root)
      @name = UnicodeOrId.new(@_io, self, @_root)
      @padding1 = @_io.read_bytes(((4 - _io.pos) % 4))
      @format_version = @_io.read_u4le
      @flags = @_io.read_u2le
      @language = @_io.read_u2le
      @value_version = @_io.read_u4le
      @characteristics = @_io.read_u4le
      @value = @_io.read_bytes(value_size)
      @padding2 = @_io.read_bytes(((4 - _io.pos) % 4))
      self
    end

    ##
    # Numeric type IDs in range of [0..0xff] are reserved for
    # system usage in Windows, and there are some predefined,
    # well-known values in that range. This instance allows to get
    # it as enum value, if applicable.
    def type_as_predef
      return @type_as_predef unless @type_as_predef.nil?
      if  ((!(type.is_string)) && (type.as_numeric <= 255)) 
        @type_as_predef = Kaitai::Struct::Stream::resolve_enum(PREDEF_TYPES, type.as_numeric)
      end
      @type_as_predef
    end

    ##
    # Size of resource value that follows the header
    attr_reader :value_size

    ##
    # Size of this header (i.e. every field except `value` and an
    # optional padding after it)
    attr_reader :header_size
    attr_reader :type
    attr_reader :name
    attr_reader :padding1
    attr_reader :format_version
    attr_reader :flags
    attr_reader :language

    ##
    # Version for value, as specified by a user.
    attr_reader :value_version

    ##
    # Extra 4 bytes that can be used by user for any purpose.
    attr_reader :characteristics
    attr_reader :value
    attr_reader :padding2
  end

  ##
  # Resources use a special serialization of names and types: they
  # can be either a number or a string.
  # 
  # Use `is_string` to check which kind we've got here, and then use
  # `as_numeric` or `as_string` to get relevant value.
  class UnicodeOrId < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      if save_pos1 >= 0
        @first = @_io.read_u2le
      end
      if !(is_string)
        @as_numeric = @_io.read_u2le
      end
      if is_string
        @rest = []
        i = 0
        begin
          _ = @_io.read_u2le
          @rest << _
          i += 1
        end until _ == 0
      end
      if  ((is_string) && (save_pos2 >= 0)) 
        @noop = @_io.read_bytes(0)
      end
      self
    end
    def save_pos1
      return @save_pos1 unless @save_pos1.nil?
      @save_pos1 = _io.pos
      @save_pos1
    end
    def save_pos2
      return @save_pos2 unless @save_pos2.nil?
      @save_pos2 = _io.pos
      @save_pos2
    end
    def is_string
      return @is_string unless @is_string.nil?
      @is_string = first != 65535
      @is_string
    end
    def as_string
      return @as_string unless @as_string.nil?
      if is_string
        _pos = @_io.pos
        @_io.seek(save_pos1)
        @as_string = (@_io.read_bytes(((save_pos2 - save_pos1) - 2))).force_encoding("UTF-16LE")
        @_io.seek(_pos)
      end
      @as_string
    end
    attr_reader :first
    attr_reader :as_numeric
    attr_reader :rest
    attr_reader :noop
  end
  attr_reader :resources
end