.lzh file format of LHA (AKA LHarc) by Yoshizaki Haruyasu: Ruby parsing library

LHA (LHarc, LZH) is a file format used by a popular freeware eponymous archiver, created in 1988 by Haruyasu Yoshizaki. Over the years, many ports and implementations were developed, sporting many extensions to original 1988 LZH.

File format is pretty simple and essentially consists of a stream of records.

Application

LHA (AKA LHarc) by Yoshizaki Haruyasu

File extension

lzh

KS implementation details

License: CC0-1.0

References

This page hosts a formal specification of .lzh file format of LHA (AKA LHarc) by Yoshizaki Haruyasu 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 = Lzh.from_file("path/to/local/file.lzh")

Or parse structure from a string of bytes:

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

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

data.entries # => get entries

Ruby source code to parse .lzh file format of LHA (AKA LHarc) by Yoshizaki Haruyasu

lzh.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


##
# LHA (LHarc, LZH) is a file format used by a popular freeware
# eponymous archiver, created in 1988 by Haruyasu Yoshizaki. Over the
# years, many ports and implementations were developed, sporting many
# extensions to original 1988 LZH.
# 
# File format is pretty simple and essentially consists of a stream of
# records.
class Lzh < Kaitai::Struct::Struct
  def initialize(_io, _parent = nil, _root = self)
    super(_io, _parent, _root)
    _read
  end

  def _read
    @entries = []
    i = 0
    while not @_io.eof?
      @entries << Record.new(@_io, self, @_root)
      i += 1
    end
    self
  end
  class Record < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @header_len = @_io.read_u1
      if header_len > 0
        @file_record = FileRecord.new(@_io, self, @_root)
      end
      self
    end
    attr_reader :header_len
    attr_reader :file_record
  end
  class FileRecord < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @_raw_header = @_io.read_bytes((_parent.header_len - 1))
      _io__raw_header = Kaitai::Struct::Stream.new(@_raw_header)
      @header = Header.new(_io__raw_header, self, @_root)
      if header.header1.lha_level == 0
        @file_uncompr_crc16 = @_io.read_u2le
      end
      @body = @_io.read_bytes(header.header1.file_size_compr)
      self
    end
    attr_reader :header
    attr_reader :file_uncompr_crc16
    attr_reader :body
    attr_reader :_raw_header
  end
  class Header < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @header1 = Header1.new(@_io, self, @_root)
      if header1.lha_level == 0
        @filename_len = @_io.read_u1
      end
      if header1.lha_level == 0
        @filename = (@_io.read_bytes(filename_len)).force_encoding("ASCII")
      end
      if header1.lha_level == 2
        @file_uncompr_crc16 = @_io.read_u2le
      end
      if header1.lha_level == 2
        @os = @_io.read_u1
      end
      if header1.lha_level == 2
        @ext_header_size = @_io.read_u2le
      end
      self
    end

    ##
    # Level-neutral header, same for all LHA levels. Subsequent fields order and meaning varies, based on LHA level specified in this header.
    attr_reader :header1
    attr_reader :filename_len
    attr_reader :filename
    attr_reader :file_uncompr_crc16
    attr_reader :os
    attr_reader :ext_header_size
  end
  class Header1 < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @header_checksum = @_io.read_u1
      @method_id = (@_io.read_bytes(5)).force_encoding("ASCII")
      @file_size_compr = @_io.read_u4le
      @file_size_uncompr = @_io.read_u4le
      @_raw_file_timestamp = @_io.read_bytes(4)
      _io__raw_file_timestamp = Kaitai::Struct::Stream.new(@_raw_file_timestamp)
      @file_timestamp = DosDatetime.new(_io__raw_file_timestamp)
      @attr = @_io.read_u1
      @lha_level = @_io.read_u1
      self
    end
    attr_reader :header_checksum
    attr_reader :method_id

    ##
    # Compressed file size
    attr_reader :file_size_compr

    ##
    # Uncompressed file size
    attr_reader :file_size_uncompr

    ##
    # Original file date/time
    attr_reader :file_timestamp

    ##
    # File or directory attribute
    attr_reader :attr
    attr_reader :lha_level
    attr_reader :_raw_file_timestamp
  end
  attr_reader :entries
end