Windows shell link file: Ruby parsing library

Windows .lnk files (AKA "shell link" file) are most frequently used in Windows shell to create "shortcuts" to another files, usually for purposes of running a program from some other directory, sometimes with certain preconfigured arguments and some other options.

File extension

lnk

KS implementation details

License: CC0-1.0

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

Usage

Parse a local file and get structure in memory:

data = WindowsLnkFile.from_file("path/to/local/file.lnk")

Or parse structure from a string of bytes:

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

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

data.header # => get header

Ruby source code to parse Windows shell link file

windows_lnk_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.7')
  raise "Incompatible Kaitai Struct Ruby API: 0.7 or later is required, but you have #{Kaitai::Struct::VERSION}"
end


##
# Windows .lnk files (AKA "shell link" file) are most frequently used
# in Windows shell to create "shortcuts" to another files, usually for
# purposes of running a program from some other directory, sometimes
# with certain preconfigured arguments and some other options.
# @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Source
class WindowsLnkFile < Kaitai::Struct::Struct

  WINDOW_STATE = {
    1 => :window_state_normal,
    3 => :window_state_maximized,
    7 => :window_state_min_no_active,
  }
  I__WINDOW_STATE = WINDOW_STATE.invert

  DRIVE_TYPES = {
    0 => :drive_types_unknown,
    1 => :drive_types_no_root_dir,
    2 => :drive_types_removable,
    3 => :drive_types_fixed,
    4 => :drive_types_remote,
    5 => :drive_types_cdrom,
    6 => :drive_types_ramdisk,
  }
  I__DRIVE_TYPES = DRIVE_TYPES.invert
  def initialize(_io, _parent = nil, _root = self)
    super(_io, _parent, _root)
    _read
  end

  def _read
    @header = FileHeader.new(@_io, self, @_root)
    if header.flags.has_link_target_id_list
      @target_id_list = LinkTargetIdList.new(@_io, self, @_root)
    end
    if header.flags.has_link_info
      @info = LinkInfo.new(@_io, self, @_root)
    end
    if header.flags.has_name
      @name = StringData.new(@_io, self, @_root)
    end
    if header.flags.has_rel_path
      @rel_path = StringData.new(@_io, self, @_root)
    end
    if header.flags.has_work_dir
      @work_dir = StringData.new(@_io, self, @_root)
    end
    if header.flags.has_arguments
      @arguments = StringData.new(@_io, self, @_root)
    end
    if header.flags.has_icon_location
      @icon_location = StringData.new(@_io, self, @_root)
    end
    self
  end

  ##
  # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.2
  class LinkTargetIdList < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @len_id_list = @_io.read_u2le
      @_raw_id_list = @_io.read_bytes(len_id_list)
      io = Kaitai::Struct::Stream.new(@_raw_id_list)
      @id_list = WindowsShellItems.new(io)
      self
    end
    attr_reader :len_id_list
    attr_reader :id_list
    attr_reader :_raw_id_list
  end
  class StringData < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @chars_str = @_io.read_u2le
      @str = (@_io.read_bytes((chars_str * 2))).force_encoding("UTF-16LE")
      self
    end
    attr_reader :chars_str
    attr_reader :str
  end

  ##
  # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.3
  class LinkInfo < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @len_all = @_io.read_u4le
      @_raw_all = @_io.read_bytes((len_all - 4))
      io = Kaitai::Struct::Stream.new(@_raw_all)
      @all = All.new(io, self, @_root)
      self
    end

    ##
    # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.3.1
    class VolumeIdBody < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @drive_type = Kaitai::Struct::Stream::resolve_enum(DRIVE_TYPES, @_io.read_u4le)
        @drive_serial_number = @_io.read_u4le
        @ofs_volume_label = @_io.read_u4le
        if is_unicode
          @ofs_volume_label_unicode = @_io.read_u4le
        end
        self
      end
      def is_unicode
        return @is_unicode unless @is_unicode.nil?
        @is_unicode = ofs_volume_label == 20
        @is_unicode
      end
      def volume_label_ansi
        return @volume_label_ansi unless @volume_label_ansi.nil?
        if !(is_unicode)
          _pos = @_io.pos
          @_io.seek((ofs_volume_label - 4))
          @volume_label_ansi = (@_io.read_bytes_term(0, false, true, true)).force_encoding("cp437")
          @_io.seek(_pos)
        end
        @volume_label_ansi
      end
      attr_reader :drive_type
      attr_reader :drive_serial_number
      attr_reader :ofs_volume_label
      attr_reader :ofs_volume_label_unicode
    end

    ##
    # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.3
    class All < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @len_header = @_io.read_u4le
        @_raw_header = @_io.read_bytes((len_header - 8))
        io = Kaitai::Struct::Stream.new(@_raw_header)
        @header = Header.new(io, self, @_root)
        self
      end
      def volume_id
        return @volume_id unless @volume_id.nil?
        if header.flags.has_volume_id_and_local_base_path
          _pos = @_io.pos
          @_io.seek((header.ofs_volume_id - 4))
          @volume_id = VolumeIdSpec.new(@_io, self, @_root)
          @_io.seek(_pos)
        end
        @volume_id
      end
      def local_base_path
        return @local_base_path unless @local_base_path.nil?
        if header.flags.has_volume_id_and_local_base_path
          _pos = @_io.pos
          @_io.seek((header.ofs_local_base_path - 4))
          @local_base_path = @_io.read_bytes_term(0, false, true, true)
          @_io.seek(_pos)
        end
        @local_base_path
      end
      attr_reader :len_header
      attr_reader :header
      attr_reader :_raw_header
    end

    ##
    # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.3.1
    class VolumeIdSpec < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @len_all = @_io.read_u4le
        @_raw_body = @_io.read_bytes((len_all - 4))
        io = Kaitai::Struct::Stream.new(@_raw_body)
        @body = VolumeIdBody.new(io, self, @_root)
        self
      end
      attr_reader :len_all
      attr_reader :body
      attr_reader :_raw_body
    end

    ##
    # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.3
    class LinkInfoFlags < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @reserved1 = @_io.read_bits_int(6)
        @has_common_net_rel_link = @_io.read_bits_int(1) != 0
        @has_volume_id_and_local_base_path = @_io.read_bits_int(1) != 0
        @reserved2 = @_io.read_bits_int(24)
        self
      end
      attr_reader :reserved1
      attr_reader :has_common_net_rel_link
      attr_reader :has_volume_id_and_local_base_path
      attr_reader :reserved2
    end

    ##
    # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.3
    class Header < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @flags = LinkInfoFlags.new(@_io, self, @_root)
        @ofs_volume_id = @_io.read_u4le
        @ofs_local_base_path = @_io.read_u4le
        @ofs_common_net_rel_link = @_io.read_u4le
        @ofs_common_path_suffix = @_io.read_u4le
        if !(_io.eof?)
          @ofs_local_base_path_unicode = @_io.read_u4le
        end
        if !(_io.eof?)
          @ofs_common_path_suffix_unicode = @_io.read_u4le
        end
        self
      end
      attr_reader :flags
      attr_reader :ofs_volume_id
      attr_reader :ofs_local_base_path
      attr_reader :ofs_common_net_rel_link
      attr_reader :ofs_common_path_suffix
      attr_reader :ofs_local_base_path_unicode
      attr_reader :ofs_common_path_suffix_unicode
    end
    attr_reader :len_all
    attr_reader :all
    attr_reader :_raw_all
  end

  ##
  # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.1.1
  class LinkFlags < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @is_unicode = @_io.read_bits_int(1) != 0
      @has_icon_location = @_io.read_bits_int(1) != 0
      @has_arguments = @_io.read_bits_int(1) != 0
      @has_work_dir = @_io.read_bits_int(1) != 0
      @has_rel_path = @_io.read_bits_int(1) != 0
      @has_name = @_io.read_bits_int(1) != 0
      @has_link_info = @_io.read_bits_int(1) != 0
      @has_link_target_id_list = @_io.read_bits_int(1) != 0
      @_unnamed8 = @_io.read_bits_int(16)
      @reserved = @_io.read_bits_int(5)
      @keep_local_id_list_for_unc_target = @_io.read_bits_int(1) != 0
      @_unnamed11 = @_io.read_bits_int(2)
      self
    end
    attr_reader :is_unicode
    attr_reader :has_icon_location
    attr_reader :has_arguments
    attr_reader :has_work_dir
    attr_reader :has_rel_path
    attr_reader :has_name
    attr_reader :has_link_info
    attr_reader :has_link_target_id_list
    attr_reader :_unnamed8
    attr_reader :reserved
    attr_reader :keep_local_id_list_for_unc_target
    attr_reader :_unnamed11
  end

  ##
  # @see https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-SHLLINK/[MS-SHLLINK].pdf Section 2.1
  class FileHeader < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @len_header = @_io.ensure_fixed_contents([76, 0, 0, 0].pack('C*'))
      @link_clsid = @_io.ensure_fixed_contents([1, 20, 2, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 70].pack('C*'))
      @_raw_flags = @_io.read_bytes(4)
      io = Kaitai::Struct::Stream.new(@_raw_flags)
      @flags = LinkFlags.new(io, self, @_root)
      @file_attrs = @_io.read_u4le
      @time_creation = @_io.read_u8le
      @time_access = @_io.read_u8le
      @time_write = @_io.read_u8le
      @target_file_size = @_io.read_u4le
      @icon_index = @_io.read_s4le
      @show_command = Kaitai::Struct::Stream::resolve_enum(WINDOW_STATE, @_io.read_u4le)
      @hotkey = @_io.read_u2le
      @reserved = @_io.ensure_fixed_contents([0, 0, 0, 0, 0, 0, 0, 0, 0, 0].pack('C*'))
      self
    end

    ##
    # Technically, a size of the header, but in reality, it's
    # fixed by standard.
    attr_reader :len_header

    ##
    # 16-byte class identified (CLSID), reserved for Windows shell
    # link files.
    attr_reader :link_clsid
    attr_reader :flags
    attr_reader :file_attrs
    attr_reader :time_creation
    attr_reader :time_access
    attr_reader :time_write

    ##
    # Lower 32 bits of the size of the file that this link targets
    attr_reader :target_file_size

    ##
    # Index of an icon to use from target file
    attr_reader :icon_index

    ##
    # Window state to set after the launch of target executable
    attr_reader :show_command
    attr_reader :hotkey
    attr_reader :reserved
    attr_reader :_raw_flags
  end
  attr_reader :header
  attr_reader :target_id_list
  attr_reader :info
  attr_reader :name
  attr_reader :rel_path
  attr_reader :work_dir
  attr_reader :arguments
  attr_reader :icon_location
end