Class: Cabriolet::HLP::Decompressor

Inherits:
Object
  • Object
show all
Defined in:
lib/cabriolet/hlp/decompressor.rb

Overview

Decompressor is the main interface for HLP file operations

HLP files use LZSS compression with MODE_MSHELP and contain an internal file system. Files are decompressed using the Decompressors::LZSS class.

NOTE: This implementation is based on the knowledge that HLP files use LZSS compression with MODE_MSHELP, but cannot be fully validated due to lack of test fixtures and incomplete libmspack implementation.

Constant Summary collapse

DEFAULT_BUFFER_SIZE =

Input buffer size for decompression

2048

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io_system = nil) ⇒ Decompressor

Initialize a new HLP decompressor

Parameters:

  • io_system (System::IOSystem, nil) (defaults to: nil)

    Custom I/O system or nil for default



24
25
26
27
28
# File 'lib/cabriolet/hlp/decompressor.rb', line 24

def initialize(io_system = nil)
  @io_system = io_system || System::IOSystem.new
  @parser = Parser.new(@io_system)
  @buffer_size = DEFAULT_BUFFER_SIZE
end

Instance Attribute Details

#buffer_sizeObject

Returns the value of attribute buffer_size.



15
16
17
# File 'lib/cabriolet/hlp/decompressor.rb', line 15

def buffer_size
  @buffer_size
end

#io_systemObject (readonly)

Returns the value of attribute io_system.



14
15
16
# File 'lib/cabriolet/hlp/decompressor.rb', line 14

def io_system
  @io_system
end

#parserObject (readonly)

Returns the value of attribute parser.



14
15
16
# File 'lib/cabriolet/hlp/decompressor.rb', line 14

def parser
  @parser
end

Instance Method Details

#close(_header) ⇒ void

This method returns an undefined value.

Close an HLP file (no-op for compatibility)

Parameters:



45
46
47
48
49
# File 'lib/cabriolet/hlp/decompressor.rb', line 45

def close(_header)
  # No resources to free in the header itself
  # File handles are managed separately during extraction
  nil
end

#extract_all(header, output_dir) ⇒ Integer

Extract all files from HLP archive

Parameters:

  • header (Models::HLPHeader)

    HLP header from open()

  • output_dir (String)

    Directory to extract files to

Returns:

  • (Integer)

    Number of files extracted

Raises:

  • (Errors::DecompressionError)

    if extraction fails



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/cabriolet/hlp/decompressor.rb', line 127

def extract_all(header, output_dir)
  raise ArgumentError, "Header must not be nil" unless header
  raise ArgumentError, "Output directory must not be nil" unless
    output_dir

  # Create output directory if needed
  FileUtils.mkdir_p(output_dir)

  extracted = 0
  header.files.each do |hlp_file|
    output_path = ::File.join(output_dir, hlp_file.filename)

    # Create subdirectories if needed
    output_subdir = ::File.dirname(output_path)
    FileUtils.mkdir_p(output_subdir)

    extract_file(header, hlp_file, output_path)
    extracted += 1
  end

  extracted
end

#extract_file(header, hlp_file, output_path) ⇒ Integer

Extract a file from HLP archive

Parameters:

  • header (Models::HLPHeader)

    HLP header from open()

  • hlp_file (Models::HLPFile)

    File to extract from archive

  • output_path (String)

    Where to write the extracted file

Returns:

  • (Integer)

    Number of bytes written

Raises:

  • (Errors::DecompressionError)

    if extraction fails



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/cabriolet/hlp/decompressor.rb', line 58

def extract_file(header, hlp_file, output_path)
  raise ArgumentError, "Header must not be nil" unless header
  raise ArgumentError, "HLP file must not be nil" unless hlp_file
  raise ArgumentError, "Output path must not be nil" unless output_path

  input_handle = @io_system.open(header.filename, Constants::MODE_READ)
  output_handle = @io_system.open(output_path, Constants::MODE_WRITE)

  begin
    # Seek to file data
    @io_system.seek(input_handle, hlp_file.offset,
                    Constants::SEEK_START)

    bytes_written = if hlp_file.compressed?
                      decompress_file(input_handle, output_handle,
                                      hlp_file)
                    else
                      copy_file(input_handle, output_handle, hlp_file)
                    end

    # Verify size if expected
    if bytes_written != hlp_file.length && Cabriolet.verbose
      warn "[Cabriolet] WARNING: extracted #{bytes_written} bytes, " \
           "expected #{hlp_file.length} bytes"
    end

    bytes_written
  ensure
    @io_system.close(input_handle) if input_handle
    @io_system.close(output_handle) if output_handle
  end
end

#extract_file_to_memory(header, hlp_file) ⇒ String

Extract a file to memory

Parameters:

Returns:

  • (String)

    Extracted data

Raises:

  • (Errors::DecompressionError)

    if extraction fails



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/cabriolet/hlp/decompressor.rb', line 97

def extract_file_to_memory(header, hlp_file)
  raise ArgumentError, "Header must not be nil" unless header
  raise ArgumentError, "HLP file must not be nil" unless hlp_file

  input_handle = @io_system.open(header.filename, Constants::MODE_READ)
  output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)

  begin
    # Seek to file data
    @io_system.seek(input_handle, hlp_file.offset,
                    Constants::SEEK_START)

    if hlp_file.compressed?
      decompress_file(input_handle, output_handle, hlp_file)
    else
      copy_file(input_handle, output_handle, hlp_file)
    end

    output_handle.data
  ensure
    @io_system.close(input_handle) if input_handle
  end
end

#open(filename) ⇒ Models::HLPHeader

Open and parse an HLP file

Parameters:

  • filename (String)

    Path to the HLP file

Returns:

Raises:

  • (Errors::ParseError)

    if the file is not a valid HLP



35
36
37
38
39
# File 'lib/cabriolet/hlp/decompressor.rb', line 35

def open(filename)
  header = @parser.parse(filename)
  header.filename = filename
  header
end