Class: Cabriolet::LIT::Decompressor

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

Overview

Decompressor for Microsoft Reader LIT files

Handles complete LIT file extraction including:

  • Parsing complex LIT structure with Parser

  • DataSpace/Storage sections with transform layers

  • LZX decompression with ResetTable

  • Manifest-based filename restoration

  • Section caching for efficiency

Based on the openclit/SharpLit reference implementation.

NOTE: DES encryption (DRM) is not supported.

Constant Summary collapse

DEFAULT_BUFFER_SIZE =

Default buffer size for decompression

8192

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io_system = nil, algorithm_factory = nil) ⇒ Decompressor



29
30
31
32
33
34
35
# File 'lib/cabriolet/lit/decompressor.rb', line 29

def initialize(io_system = nil, algorithm_factory = nil)
  @io_system = io_system || System::IOSystem.new
  @algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
  @parser = Parser.new(@io_system)
  @section_cache = {}
  @buffer_size = DEFAULT_BUFFER_SIZE
end

Instance Attribute Details

#buffer_sizeObject

Returns the value of attribute buffer_size.



24
25
26
# File 'lib/cabriolet/lit/decompressor.rb', line 24

def buffer_size
  @buffer_size
end

#io_systemObject (readonly)

Returns the value of attribute io_system.



23
24
25
# File 'lib/cabriolet/lit/decompressor.rb', line 23

def io_system
  @io_system
end

#parserObject (readonly)

Returns the value of attribute parser.



23
24
25
# File 'lib/cabriolet/lit/decompressor.rb', line 23

def parser
  @parser
end

Instance Method Details

#close(_lit_file) ⇒ void

This method returns an undefined value.

Close a LIT file (no-op for compatibility)



63
64
65
66
67
68
# File 'lib/cabriolet/lit/decompressor.rb', line 63

def close(_lit_file)
  # No resources to free in the file object itself
  # File handles are managed separately during extraction
  @section_cache.clear
  nil
end

#extract(lit_file, file, output_path) ⇒ Integer

Extract a file from LIT archive (wrapper for extract_file)

Raises:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/cabriolet/lit/decompressor.rb', line 79

def extract(lit_file, file, output_path)
  raise ArgumentError, "Header must not be nil" unless lit_file
  raise ArgumentError, "File must not be nil" unless file
  raise ArgumentError, "Output path must not be nil" unless output_path

  # Check for encryption
  if lit_file.encrypted?
    raise NotImplementedError,
          "Encrypted sections not yet supported. " \
          "DRM level: #{lit_file.drm_level}"
  end

  # Use extract_file with file name
  internal_name = file.respond_to?(:name) ? file.name : file.to_s
  extract_file(lit_file, internal_name, output_path)
end

#extract_all(lit_file, output_dir, use_manifest: true) ⇒ Integer

Extract all files from LIT archive

Raises:



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/cabriolet/lit/decompressor.rb', line 145

def extract_all(lit_file, output_dir, use_manifest: true)
  raise ArgumentError, "Header must not be nil" unless lit_file

  unless output_dir
    raise ArgumentError,
          "Output directory must not be nil"
  end

  ::FileUtils.mkdir_p(output_dir)

  extracted = 0

  # Extract each directory entry
  lit_file.directory.entries.each do |entry|
    # Skip root entry and directories (ending with /)
    next if entry.root? || entry.name.end_with?("/")

    # Determine output filename
    if use_manifest && lit_file.manifest
      mapping = lit_file.manifest.find_by_internal(entry.name)
      filename = mapping ? mapping.original_name : entry.name
    else
      filename = entry.name
    end

    # Sanitize filename and convert path separators
    # Replace :: prefix and convert / to proper path separator
    filename = sanitize_path(filename)

    # Create output path (join with output_dir)
    output_path = ::File.join(output_dir, filename)

    # Create subdirectories if needed
    file_dir = ::File.dirname(output_path)
    ::FileUtils.mkdir_p(file_dir) unless ::File.directory?(file_dir)

    # Extract file
    extract_file(lit_file, entry.name, output_path)
    extracted += 1
  end

  extracted
end

#extract_file(lit_file, internal_name, output_path) ⇒ Integer

Extract a file by name from LIT archive

Raises:



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/cabriolet/lit/decompressor.rb', line 103

def extract_file(lit_file, internal_name, output_path)
  raise ArgumentError, "LIT file required" unless lit_file
  raise ArgumentError, "Internal name required" unless internal_name
  raise ArgumentError, "Output path required" unless output_path

  # Find directory entry
  entry = lit_file.directory.find(internal_name)
  unless entry
    raise Cabriolet::DecompressionError,
          "File not found: #{internal_name}"
  end

  # Get section data (cached or decompressed)
  section_data = get_section_data(lit_file, entry.section)

  # Extract file from section
  file_data = section_data[entry.offset, entry.size]

  # Check if extraction was successful
  unless file_data
    raise Cabriolet::DecompressionError,
          "Failed to extract file #{entry.name}: " \
          "offset=#{entry.offset}, size=#{entry.size}, section_data_size=#{section_data&.bytesize || 0}"
  end

  # Write to output
  output_handle = @io_system.open(output_path, Constants::MODE_WRITE)
  begin
    @io_system.write(output_handle, file_data)
  ensure
    @io_system.close(output_handle)
  end

  file_data.bytesize
end

#list_files(lit_file, use_manifest: true) ⇒ Array<Hash>

List all files in LIT archive

Raises:



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/cabriolet/lit/decompressor.rb', line 194

def list_files(lit_file, use_manifest: true)
  raise ArgumentError, "LIT file required" unless lit_file

  lit_file.directory.entries.reject(&:root?).map do |entry|
    info = {
      internal_name: entry.name,
      section: entry.section,
      offset: entry.offset,
      size: entry.size,
    }

    if use_manifest && lit_file.manifest
      mapping = lit_file.manifest.find_by_internal(entry.name)
      if mapping
        info[:original_name] = mapping.original_name
        info[:content_type] = mapping.content_type
      end
    end

    info
  end
end

#open(filename) ⇒ Models::LITFile

Open and parse a LIT file

Raises:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/cabriolet/lit/decompressor.rb', line 43

def open(filename)
  lit_file = @parser.parse(filename)

  # Store filename for later extraction
  lit_file.instance_variable_set(:@filename, filename)

  # Check for DRM
  if lit_file.encrypted?
    raise NotImplementedError,
          "DES-encrypted LIT files not supported. " \
          "DRM level: #{lit_file.drm_level}"
  end

  lit_file
end