Class: Cabriolet::CAB::Extractor

Inherits:
Object
  • Object
show all
Defined in:
lib/cabriolet/cab/extractor.rb

Overview

Extractor handles the extraction of files from cabinets

Defined Under Namespace

Classes: BlockReader

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io_system, decompressor) ⇒ Extractor

Initialize a new extractor

Parameters:



16
17
18
19
20
21
22
23
24
25
# File 'lib/cabriolet/cab/extractor.rb', line 16

def initialize(io_system, decompressor)
  @io_system = io_system
  @decompressor = decompressor

  # State reuse for multi-file extraction (like libmspack self->d)
  @current_folder = nil
  @current_decomp = nil
  @current_input = nil
  @current_offset = 0
end

Instance Attribute Details

#decompressorObject (readonly)

Returns the value of attribute decompressor.



10
11
12
# File 'lib/cabriolet/cab/extractor.rb', line 10

def decompressor
  @decompressor
end

#io_systemObject (readonly)

Returns the value of attribute io_system.



10
11
12
# File 'lib/cabriolet/cab/extractor.rb', line 10

def io_system
  @io_system
end

Instance Method Details

#extract_all(cabinet, output_dir, **options) ⇒ Integer

Extract all files from a cabinet

Parameters:

  • cabinet (Models::Cabinet)

    Cabinet to extract from

  • output_dir (String)

    Directory to extract to

  • options (Hash)

    Extraction options

Options Hash (**options):

  • :preserve_paths (Boolean)

    Preserve directory structure (default: true)

  • :set_timestamps (Boolean)

    Set file modification times (default: true)

  • :progress (Proc)

    Progress callback

Returns:

  • (Integer)

    Number of files extracted



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
188
189
# File 'lib/cabriolet/cab/extractor.rb', line 154

def extract_all(cabinet, output_dir, **options)
  preserve_paths = options.fetch(:preserve_paths, true)
  set_timestamps = options.fetch(:set_timestamps, true)
  progress = options[:progress]

  # Create output directory
  FileUtils.mkdir_p(output_dir) unless ::File.directory?(output_dir)

  count = 0
  cabinet.files.each do |file|
    # Determine output path
    output_path = if preserve_paths
                    ::File.join(output_dir, file.filename)
                  else
                    ::File.join(output_dir,
                                ::File.basename(file.filename))
                  end

    # Extract file
    extract_file(file, output_path, **options)

    # Set timestamp if requested
    if set_timestamps && file.modification_time
      ::File.utime(file.modification_time, file.modification_time,
                   output_path)
    end

    # Set file permissions based on attributes
    set_file_attributes(output_path, file)

    count += 1
    progress&.call(file, count, cabinet.files.size)
  end

  count
end

#extract_file(file, output_path, **options) ⇒ Integer

Extract a single file from the cabinet

Parameters:

  • file (Models::File)

    File to extract

  • output_path (String)

    Where to write the file

  • options (Hash)

    Extraction options

Options Hash (**options):

  • :salvage (Boolean)

    Enable salvage mode

Returns:

  • (Integer)

    Number of bytes extracted

Raises:



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
90
91
92
93
94
95
96
97
98
99
100
101
102
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
138
139
140
141
142
143
# File 'lib/cabriolet/cab/extractor.rb', line 34

def extract_file(file, output_path, **options)
  salvage = options[:salvage] || @decompressor.salvage
  folder = file.folder

  # Validate file
  raise Cabriolet::ArgumentError, "File has no folder" unless folder

  if file.offset > Constants::LENGTH_MAX
    raise DecompressionError,
          "File offset beyond 2GB limit"
  end

  # Check file length
  filelen = file.length
  if filelen > (Constants::LENGTH_MAX - file.offset)
    unless salvage
      raise DecompressionError,
            "File length exceeds 2GB limit"
    end

    filelen = Constants::LENGTH_MAX - file.offset
  end

  # Check for merge requirements
  if folder.needs_prev_merge?
    raise DecompressionError,
          "File requires previous cabinet, cabinet set is incomplete"
  end

  # Check file fits within folder
  unless salvage
    max_len = folder.num_blocks * Constants::BLOCK_MAX
    if file.offset > max_len || filelen > (max_len - file.offset)
      raise DecompressionError, "File extends beyond folder data"
    end
  end

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

  # Check if we need to change folder or reset (libmspack lines 1076-1078)
  if ENV["DEBUG_BLOCK"]
    warn "DEBUG extract_file: Checking reset condition for file #{file.filename} (offset=#{file.offset}, length=#{file.length})"
    warn "  @current_folder == folder: #{@current_folder == folder} (current=#{@current_folder.object_id}, new=#{folder.object_id})"
    warn "  @current_offset (#{@current_offset}) > file.offset (#{file.offset}): #{@current_offset > file.offset}"
    warn "  @current_decomp.nil?: #{@current_decomp.nil?}"
    warn "  Reset needed?: #{@current_folder != folder || @current_offset > file.offset || !@current_decomp}"
  end

  if @current_folder != folder || @current_offset > file.offset || !@current_decomp
    if ENV["DEBUG_BLOCK"]
      warn "DEBUG extract_file: RESETTING state (creating new BlockReader)"
    end

    # Reset state
    @current_input&.close
    @current_input = nil
    @current_decomp = nil

    # Create new input (libmspack lines 1092-1095)
    # This BlockReader will be REUSED across all files in this folder
    @current_input = BlockReader.new(@io_system, folder.data,
                                     folder.num_blocks, salvage)
    @current_folder = folder
    @current_offset = 0

    # Create decompressor ONCE and reuse it (this is the key fix!)
    # The decompressor maintains bitstream state across files
    @current_decomp = @decompressor.create_decompressor(folder,
                                                        @current_input, nil)
  elsif ENV["DEBUG_BLOCK"]
    warn "DEBUG extract_file: NOT resetting (reusing existing BlockReader and decompressor)"
  end

  # Skip ahead if needed (libmspack lines 1130-1134)
  if file.offset > @current_offset
    skip_bytes = file.offset - @current_offset

    # Decompress with NULL output to skip (libmspack line 1130: self->d->outfh = NULL)
    null_output = System::MemoryHandle.new("", Constants::MODE_WRITE)

    # Reuse existing decompressor, change output to NULL
    @current_decomp.instance_variable_set(:@output, null_output)

    # Set output length for LZX frame limiting
    @current_decomp.set_output_length(skip_bytes) if @current_decomp.respond_to?(:set_output_length)

    @current_decomp.decompress(skip_bytes)
    @current_offset += skip_bytes
  end

  # Extract actual file (libmspack lines 1137-1141)
  output_fh = @io_system.open(output_path, Constants::MODE_WRITE)

  begin
    # Reuse existing decompressor, change output to real file
    @current_decomp.instance_variable_set(:@output, output_fh)

    # Set output length for LZX frame limiting
    @current_decomp.set_output_length(filelen) if @current_decomp.respond_to?(:set_output_length)

    @current_decomp.decompress(filelen)
    @current_offset += filelen
  ensure
    output_fh.close
  end

  filelen
end