cabriolet 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +700 -38
  3. data/lib/cabriolet/algorithm_factory.rb +250 -0
  4. data/lib/cabriolet/base_compressor.rb +206 -0
  5. data/lib/cabriolet/binary/bitstream.rb +154 -14
  6. data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
  7. data/lib/cabriolet/binary/chm_structures.rb +2 -2
  8. data/lib/cabriolet/binary/hlp_structures.rb +258 -37
  9. data/lib/cabriolet/binary/lit_structures.rb +231 -65
  10. data/lib/cabriolet/binary/oab_structures.rb +17 -1
  11. data/lib/cabriolet/cab/command_handler.rb +226 -0
  12. data/lib/cabriolet/cab/compressor.rb +35 -43
  13. data/lib/cabriolet/cab/decompressor.rb +14 -19
  14. data/lib/cabriolet/cab/extractor.rb +140 -31
  15. data/lib/cabriolet/chm/command_handler.rb +227 -0
  16. data/lib/cabriolet/chm/compressor.rb +7 -3
  17. data/lib/cabriolet/chm/decompressor.rb +39 -21
  18. data/lib/cabriolet/chm/parser.rb +5 -2
  19. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  20. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  21. data/lib/cabriolet/cli/command_registry.rb +83 -0
  22. data/lib/cabriolet/cli.rb +356 -607
  23. data/lib/cabriolet/compressors/base.rb +1 -1
  24. data/lib/cabriolet/compressors/lzx.rb +241 -54
  25. data/lib/cabriolet/compressors/mszip.rb +35 -3
  26. data/lib/cabriolet/compressors/quantum.rb +34 -45
  27. data/lib/cabriolet/decompressors/base.rb +1 -1
  28. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  29. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  30. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  31. data/lib/cabriolet/decompressors/quantum.rb +3 -2
  32. data/lib/cabriolet/errors.rb +3 -0
  33. data/lib/cabriolet/file_entry.rb +156 -0
  34. data/lib/cabriolet/file_manager.rb +144 -0
  35. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  36. data/lib/cabriolet/hlp/compressor.rb +28 -238
  37. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  38. data/lib/cabriolet/hlp/parser.rb +52 -101
  39. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  40. data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
  41. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  42. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  43. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  44. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  45. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  46. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  47. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  48. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  49. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  50. data/lib/cabriolet/huffman/tree.rb +85 -1
  51. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  52. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  53. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  54. data/lib/cabriolet/lit/command_handler.rb +221 -0
  55. data/lib/cabriolet/lit/compressor.rb +633 -38
  56. data/lib/cabriolet/lit/decompressor.rb +518 -152
  57. data/lib/cabriolet/lit/parser.rb +670 -0
  58. data/lib/cabriolet/models/hlp_file.rb +130 -29
  59. data/lib/cabriolet/models/hlp_header.rb +105 -17
  60. data/lib/cabriolet/models/lit_header.rb +212 -25
  61. data/lib/cabriolet/models/szdd_header.rb +10 -2
  62. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  63. data/lib/cabriolet/oab/command_handler.rb +257 -0
  64. data/lib/cabriolet/oab/compressor.rb +17 -8
  65. data/lib/cabriolet/oab/decompressor.rb +41 -10
  66. data/lib/cabriolet/offset_calculator.rb +81 -0
  67. data/lib/cabriolet/plugin.rb +233 -0
  68. data/lib/cabriolet/plugin_manager.rb +453 -0
  69. data/lib/cabriolet/plugin_validator.rb +422 -0
  70. data/lib/cabriolet/system/io_system.rb +3 -0
  71. data/lib/cabriolet/system/memory_handle.rb +17 -4
  72. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  73. data/lib/cabriolet/szdd/compressor.rb +15 -11
  74. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  75. data/lib/cabriolet/version.rb +1 -1
  76. data/lib/cabriolet.rb +67 -17
  77. metadata +33 -2
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ module Models
5
+ # Windows Help (WinHelp) file header model
6
+ #
7
+ # Represents the metadata of a Windows Help file (WinHelp 3.x or 4.x).
8
+ # WinHelp files contain an internal file system with |SYSTEM, |TOPIC,
9
+ # and other internal files.
10
+ class WinHelpHeader
11
+ attr_accessor :version, :magic, :directory_offset, :free_list_offset, :file_size, :filename # :winhelp3 or :winhelp4 # Magic number (0x35F3 or 0x3F5F0000)
12
+
13
+ # Internal files in the help file
14
+ # Array of hashes: { filename:, file_size:, starting_block: }
15
+ attr_accessor :internal_files
16
+
17
+ # Parsed |SYSTEM file data (if extracted)
18
+ attr_accessor :system_data
19
+
20
+ # Initialize WinHelp header
21
+ #
22
+ # @param version [Symbol] :winhelp3 or :winhelp4
23
+ # @param magic [Integer] Magic number
24
+ # @param directory_offset [Integer] Offset to internal file directory
25
+ # @param free_list_offset [Integer] Offset to free list
26
+ # @param file_size [Integer] Total file size
27
+ # @param filename [String, nil] Original filename
28
+ def initialize(
29
+ version: :winhelp3,
30
+ magic: 0,
31
+ directory_offset: 0,
32
+ free_list_offset: 0,
33
+ file_size: 0,
34
+ filename: nil
35
+ )
36
+ @version = version
37
+ @magic = magic
38
+ @directory_offset = directory_offset
39
+ @free_list_offset = free_list_offset
40
+ @file_size = file_size
41
+ @filename = filename
42
+
43
+ @internal_files = []
44
+ @system_data = nil
45
+ end
46
+
47
+ # Check if header is valid
48
+ #
49
+ # @return [Boolean] true if header appears valid
50
+ def valid?
51
+ case @version
52
+ when :winhelp3
53
+ @magic == 0x35F3
54
+ when :winhelp4
55
+ (@magic & 0xFFFF) == 0x3F5F
56
+ else
57
+ false
58
+ end
59
+ end
60
+
61
+ # Check if this is WinHelp 3.x format
62
+ #
63
+ # @return [Boolean] true if WinHelp 3.x
64
+ def winhelp3?
65
+ @version == :winhelp3
66
+ end
67
+
68
+ # Check if this is WinHelp 4.x format
69
+ #
70
+ # @return [Boolean] true if WinHelp 4.x
71
+ def winhelp4?
72
+ @version == :winhelp4
73
+ end
74
+
75
+ # Get list of internal filenames
76
+ #
77
+ # @return [Array<String>] Internal file names
78
+ def internal_filenames
79
+ @internal_files.map { |f| f[:filename] }
80
+ end
81
+
82
+ # Find internal file by name
83
+ #
84
+ # @param name [String] Internal filename (e.g., "|SYSTEM")
85
+ # @return [Hash, nil] File entry or nil if not found
86
+ def find_file(name)
87
+ @internal_files.find { |f| f[:filename] == name }
88
+ end
89
+
90
+ # Check if |SYSTEM file exists
91
+ #
92
+ # @return [Boolean] true if |SYSTEM file present
93
+ def has_system_file?
94
+ !find_file("|SYSTEM").nil?
95
+ end
96
+
97
+ # Check if |TOPIC file exists
98
+ #
99
+ # @return [Boolean] true if |TOPIC file present
100
+ def has_topic_file?
101
+ !find_file("|TOPIC").nil?
102
+ end
103
+
104
+ # Get version string
105
+ #
106
+ # @return [String] Human-readable version
107
+ def version_string
108
+ case @version
109
+ when :winhelp3
110
+ "Windows Help 3.x (16-bit)"
111
+ when :winhelp4
112
+ "Windows Help 4.x (32-bit)"
113
+ else
114
+ "Unknown"
115
+ end
116
+ end
117
+
118
+ # Get magic number as hex string
119
+ #
120
+ # @return [String] Hex representation of magic
121
+ def magic_hex
122
+ magic_int = @magic.respond_to?(:to_i) ? @magic.to_i : @magic.to_int
123
+ "0x#{magic_int.to_s(16).upcase}"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cli/base_command_handler"
4
+ require_relative "decompressor"
5
+ require_relative "compressor"
6
+
7
+ module Cabriolet
8
+ module OAB
9
+ # Command handler for OAB (Outlook Offline Address Book) format
10
+ #
11
+ # This handler implements the unified command interface for OAB files,
12
+ # wrapping the existing OAB::Decompressor and OAB::Compressor classes.
13
+ # OAB files use LZX compression for address book data.
14
+ #
15
+ # Unlike other formats, OAB is a compressed data format rather than
16
+ # an archive - the "list" command displays header information only.
17
+ #
18
+ class CommandHandler < Commands::BaseCommandHandler
19
+ # List OAB file information
20
+ #
21
+ # Displays information about the OAB file including version,
22
+ # block size, and target size.
23
+ #
24
+ # @param file [String] Path to the OAB file
25
+ # @param options [Hash] Additional options (unused)
26
+ # @return [void]
27
+ def list(file, _options = {})
28
+ validate_file_exists(file)
29
+
30
+ display_oab_info(file)
31
+ end
32
+
33
+ # Extract/decompress OAB file
34
+ #
35
+ # Decompresses the OAB file to its original form.
36
+ # Auto-detects output filename if not specified.
37
+ #
38
+ # @param file [String] Path to the OAB file
39
+ # @param output_dir [String] Output directory (not typically used for OAB)
40
+ # @param options [Hash] Additional options
41
+ # @option options [String] :output Output file path
42
+ # @option options [String] :base_file Base file for incremental patches
43
+ # @return [void]
44
+ def extract(file, output_dir = nil, options = {})
45
+ validate_file_exists(file)
46
+
47
+ output = options[:output]
48
+
49
+ # Auto-detect output name if not provided
50
+ if output.nil? && output_dir.nil?
51
+ output = auto_output_filename(file)
52
+ end
53
+
54
+ # If output_dir is specified, construct output path
55
+ if output.nil? && output_dir
56
+ base_name = File.basename(file, ".*")
57
+ output = File.join(output_dir, base_name)
58
+ end
59
+
60
+ decompressor = Decompressor.new
61
+
62
+ # Check if this is an incremental patch
63
+ if options[:base_file]
64
+ base_file = options[:base_file]
65
+ validate_file_exists(base_file)
66
+
67
+ puts "Applying incremental patch: #{file} + #{base_file} -> #{output}" if verbose?
68
+ bytes = decompressor.decompress_incremental(file, base_file, output)
69
+ puts "Applied patch to #{output} (#{bytes} bytes)"
70
+ else
71
+ puts "Decompressing #{file} -> #{output}" if verbose?
72
+ bytes = decompressor.decompress(file, output)
73
+ puts "Decompressed #{file} to #{output} (#{bytes} bytes)"
74
+ end
75
+ end
76
+
77
+ # Create OAB compressed file
78
+ #
79
+ # Compresses a file using OAB LZX compression.
80
+ #
81
+ # @param output [String] Output OAB file path
82
+ # @param files [Array<String>] Input file (single file for OAB)
83
+ # @param options [Hash] Additional options
84
+ # @option options [Integer] :block_size Block size for compression
85
+ # @option options [String] :base_file Base file for creating incremental patch
86
+ # @return [void]
87
+ # @raise [ArgumentError] if no file specified or multiple files
88
+ def create(output, files = [], options = {})
89
+ raise ArgumentError, "No file specified" if files.empty?
90
+
91
+ if files.size > 1
92
+ raise ArgumentError,
93
+ "OAB format supports only one file at a time"
94
+ end
95
+
96
+ file = files.first
97
+ unless File.exist?(file)
98
+ raise ArgumentError,
99
+ "File does not exist: #{file}"
100
+ end
101
+
102
+ compressor = Compressor.new
103
+
104
+ # Auto-generate output name if not provided
105
+ if output.nil?
106
+ output = "#{file}.oab"
107
+ end
108
+
109
+ if options[:base_file]
110
+ base_file = options[:base_file]
111
+ unless File.exist?(base_file)
112
+ raise ArgumentError,
113
+ "Base file does not exist: #{base_file}"
114
+ end
115
+
116
+ puts "Creating incremental patch: #{file} - #{base_file} -> #{output}" if verbose?
117
+ bytes = compressor.compress_incremental(file, base_file, output,
118
+ **options)
119
+ puts "Created incremental patch #{output} (#{bytes} bytes)"
120
+ else
121
+ block_size = options[:block_size]
122
+ puts "Compressing #{file} -> #{output} (block_size: #{block_size || 'default'})" if verbose?
123
+ bytes = compressor.compress(file, output, **options)
124
+ puts "Compressed #{file} to #{output} (#{bytes} bytes)"
125
+ end
126
+ end
127
+
128
+ # Display detailed OAB file information
129
+ #
130
+ # @param file [String] Path to the OAB file
131
+ # @param options [Hash] Additional options (unused)
132
+ # @return [void]
133
+ def info(file, _options = {})
134
+ validate_file_exists(file)
135
+
136
+ display_oab_info(file)
137
+ end
138
+
139
+ # Test OAB file integrity
140
+ #
141
+ # Verifies the OAB file structure.
142
+ #
143
+ # @param file [String] Path to the OAB file
144
+ # @param options [Hash] Additional options (unused)
145
+ # @return [void]
146
+ def test(file, _options = {})
147
+ validate_file_exists(file)
148
+
149
+ puts "Testing #{file}..."
150
+
151
+ # Try to read and validate header
152
+ decompressor = Decompressor.new
153
+ # We can't easily test without decompressing, so we attempt to read the header
154
+ io_system = decompressor.io_system
155
+ handle = io_system.open(file, Constants::MODE_READ)
156
+
157
+ begin
158
+ header_data = io_system.read(handle, 16)
159
+ if header_data.length < 16
160
+ puts "ERROR: Failed to read OAB header"
161
+ return
162
+ end
163
+
164
+ # Check if it's a full file or patch file
165
+ full_header = Binary::OABStructures::FullHeader.read(header_data)
166
+ if full_header.valid?
167
+ puts "OK: OAB full file structure is valid"
168
+ puts "Version: #{full_header.version_hi}.#{full_header.version_lo}"
169
+ puts "Target size: #{full_header.target_size} bytes"
170
+ puts "Block max: #{full_header.block_max} bytes"
171
+ else
172
+ # Check for patch header
173
+ patch_header = Binary::OABStructures::PatchHeader.read(header_data)
174
+ if patch_header.valid?
175
+ puts "OK: OAB patch file structure is valid"
176
+ puts "Version: #{patch_header.version_hi}.#{patch_header.version_lo}"
177
+ puts "Target size: #{patch_header.target_size} bytes"
178
+ puts "Source size: #{patch_header.source_size} bytes"
179
+ else
180
+ puts "ERROR: Invalid OAB header signature"
181
+ end
182
+ end
183
+ rescue StandardError => e
184
+ puts "ERROR: OAB file validation failed: #{e.message}"
185
+ ensure
186
+ io_system.close(handle)
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ # Display OAB file information
193
+ #
194
+ # @param file [String] Path to the OAB file
195
+ # @return [void]
196
+ def display_oab_info(file)
197
+ puts "OAB File Information"
198
+ puts "=" * 50
199
+ puts "Filename: #{file}"
200
+
201
+ decompressor = Decompressor.new
202
+ io_system = decompressor.io_system
203
+ handle = io_system.open(file, Constants::MODE_READ)
204
+
205
+ begin
206
+ header_data = io_system.read(handle, 28) # Read enough for both header types
207
+
208
+ # Try full file header first
209
+ full_header = Binary::OABStructures::FullHeader.read(header_data[0,
210
+ 16])
211
+ if full_header.valid?
212
+ puts "Type: Full OAB file"
213
+ puts "Version: #{full_header.version_hi}.#{full_header.version_lo}"
214
+ puts "Target size: #{full_header.target_size} bytes"
215
+ puts "Block max: #{full_header.block_max} bytes"
216
+ return
217
+ end
218
+
219
+ # Try patch file header
220
+ patch_header = Binary::OABStructures::PatchHeader.read(header_data[0,
221
+ 28])
222
+ if patch_header.valid?
223
+ puts "Type: Incremental OAB patch"
224
+ puts "Version: #{patch_header.version_hi}.#{patch_header.version_lo}"
225
+ puts "Target size: #{patch_header.target_size} bytes"
226
+ puts "Source size: #{patch_header.source_size} bytes"
227
+ puts "Source CRC: 0x#{patch_header.source_crc.to_s(16).upcase}"
228
+ puts "Target CRC: 0x#{patch_header.target_crc.to_s(16).upcase}"
229
+ return
230
+ end
231
+
232
+ puts "Type: Unknown (invalid header)"
233
+ rescue StandardError => e
234
+ puts "Error reading OAB header: #{e.message}"
235
+ ensure
236
+ io_system.close(handle)
237
+ end
238
+ end
239
+
240
+ # Auto-detect output filename from OAB file
241
+ #
242
+ # @param file [String] Original file path
243
+ # @return [String] Detected output filename
244
+ def auto_output_filename(file)
245
+ # Remove .oab extension if present, otherwise just return the basename
246
+ base_name = File.basename(file, ".*")
247
+ # If the file doesn't end with .oab, keep the original name
248
+ if file.end_with?(".oab")
249
+ base_name
250
+ else
251
+ # Return with .dat extension (common for decompressed OAB)
252
+ "#{base_name}.dat"
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -32,8 +32,10 @@ module Cabriolet
32
32
  # Initialize OAB compressor
33
33
  #
34
34
  # @param io_system [System::IOSystem, nil] I/O system or nil for default
35
- def initialize(io_system = nil)
35
+ # @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
36
+ def initialize(io_system = nil, algorithm_factory = nil)
36
37
  @io_system = io_system || System::IOSystem.new
38
+ @algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
37
39
  @buffer_size = DEFAULT_BUFFER_SIZE
38
40
  @block_size = DEFAULT_BLOCK_SIZE
39
41
  end
@@ -47,7 +49,7 @@ module Cabriolet
47
49
  # @return [Integer] Bytes written
48
50
  # @raise [Error] if compression fails
49
51
  def compress(input_file, output_file, **options)
50
- block_size = options.fetch(:block_size, @block_size)
52
+ block_size = options[:block_size] || @block_size
51
53
 
52
54
  input_handle = @io_system.open(input_file, Constants::MODE_READ)
53
55
  output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
@@ -93,7 +95,7 @@ module Cabriolet
93
95
  # @return [Integer] Bytes written
94
96
  # @raise [Error] if compression fails
95
97
  def compress_data(data, output_file, **options)
96
- block_size = options.fetch(:block_size, @block_size)
98
+ block_size = options[:block_size] || @block_size
97
99
 
98
100
  input_handle = System::MemoryHandle.new(data, Constants::MODE_READ)
99
101
  output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
@@ -138,7 +140,7 @@ module Cabriolet
138
140
  # @return [Integer] Bytes written
139
141
  # @raise [Error] if compression fails
140
142
  def compress_incremental(input_file, base_file, output_file, **options)
141
- block_size = options.fetch(:block_size, @block_size)
143
+ block_size = options[:block_size] || @block_size
142
144
 
143
145
  # For now, just compress the new file with patch header
144
146
  # A full implementation would generate binary diffs
@@ -249,7 +251,8 @@ module Cabriolet
249
251
  compressed_data = compress_with_lzx(block_data)
250
252
 
251
253
  # Use compressed data (or original if compression fails)
252
- patch_data = compressed_data && compressed_data.bytesize < block_data.bytesize ? compressed_data : block_data
254
+ is_compressed = compressed_data && compressed_data.bytesize < block_data.bytesize
255
+ patch_data = is_compressed ? compressed_data : block_data
253
256
  patch_size = patch_data.bytesize
254
257
 
255
258
  # Calculate CRC
@@ -257,6 +260,7 @@ module Cabriolet
257
260
 
258
261
  # Write patch block header
259
262
  block_header = Binary::OABStructures::PatchBlockHeader.new
263
+ block_header.flags = is_compressed ? 1 : 0
260
264
  block_header.patch_size = patch_size
261
265
  block_header.target_size = block_size
262
266
  block_header.source_size = source_size
@@ -287,9 +291,14 @@ module Cabriolet
287
291
  output_mem = System::MemoryHandle.new("", Constants::MODE_WRITE)
288
292
 
289
293
  # Compress with LZX
290
- compressor = Compressors::LZX.new(
291
- @io_system, input_mem, output_mem, @buffer_size,
292
- window_bits: window_bits
294
+ compressor = @algorithm_factory.create(
295
+ Constants::COMP_TYPE_LZX,
296
+ :compressor,
297
+ @io_system,
298
+ input_mem,
299
+ output_mem,
300
+ @buffer_size,
301
+ window_bits: window_bits,
293
302
  )
294
303
 
295
304
  compressor.compress
@@ -25,8 +25,10 @@ module Cabriolet
25
25
  # Initialize OAB decompressor
26
26
  #
27
27
  # @param io_system [System::IOSystem, nil] I/O system or nil for default
28
- def initialize(io_system = nil)
28
+ # @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
29
+ def initialize(io_system = nil, algorithm_factory = nil)
29
30
  @io_system = io_system || System::IOSystem.new
31
+ @algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
30
32
  @buffer_size = DEFAULT_BUFFER_SIZE
31
33
  end
32
34
 
@@ -161,9 +163,9 @@ target_remaining)
161
163
  # @return [Integer] Bytes written
162
164
  def decompress_patch_block(patch_handle, base_handle, output_handle,
163
165
  block_max, target_remaining)
164
- # Read patch block header
165
- block_data = @io_system.read(patch_handle, 16)
166
- if block_data.length < 16
166
+ # Read patch block header (20 bytes with flags field)
167
+ block_data = @io_system.read(patch_handle, 20)
168
+ if block_data.length < 20
167
169
  raise Error,
168
170
  "Failed to read patch block header"
169
171
  end
@@ -177,6 +179,25 @@ target_remaining)
177
179
  raise Error, "Invalid patch block header"
178
180
  end
179
181
 
182
+ # Check if data is compressed or uncompressed
183
+ if block_header.uncompressed?
184
+ # Uncompressed data - read and write directly
185
+ data = @io_system.read(patch_handle, block_header.patch_size)
186
+ if data.length < block_header.patch_size
187
+ raise Error, "Failed to read uncompressed patch data"
188
+ end
189
+
190
+ # Verify CRC
191
+ actual_crc = Zlib.crc32(data)
192
+ if actual_crc != block_header.crc
193
+ raise Error, "CRC mismatch in patch block"
194
+ end
195
+
196
+ @io_system.write(output_handle, data)
197
+ return block_header.target_size
198
+ end
199
+
200
+ # Compressed data - use LZX decompression
180
201
  # Calculate window size for LZX
181
202
  window_size = ((block_header.source_size + 32_767) & ~32_767) +
182
203
  block_header.target_size
@@ -245,12 +266,17 @@ target_remaining)
245
266
  output_mem = System::MemoryHandle.new("", Constants::MODE_WRITE)
246
267
 
247
268
  # Decompress with LZX
248
- lzx = Decompressors::LZX.new(
249
- @io_system, input_mem, output_mem, @buffer_size,
269
+ lzx = @algorithm_factory.create(
270
+ Constants::COMP_TYPE_LZX,
271
+ :decompressor,
272
+ @io_system,
273
+ input_mem,
274
+ output_mem,
275
+ @buffer_size,
250
276
  window_bits: window_bits,
251
277
  reset_interval: 0,
252
278
  output_length: block_size,
253
- is_delta: false
279
+ is_delta: false,
254
280
  )
255
281
 
256
282
  bytes_decompressed = lzx.decompress(block_size)
@@ -286,12 +312,17 @@ target_remaining)
286
312
  output_mem = System::MemoryHandle.new("", Constants::MODE_WRITE)
287
313
 
288
314
  # Decompress with LZX DELTA (includes reference data)
289
- lzx = Decompressors::LZX.new(
290
- @io_system, input_mem, output_mem, @buffer_size,
315
+ lzx = @algorithm_factory.create(
316
+ Constants::COMP_TYPE_LZX,
317
+ :decompressor,
318
+ @io_system,
319
+ input_mem,
320
+ output_mem,
321
+ @buffer_size,
291
322
  window_bits: window_bits,
292
323
  reset_interval: 0,
293
324
  output_length: block_header.target_size,
294
- is_delta: true
325
+ is_delta: true,
295
326
  )
296
327
 
297
328
  # For patches, we'd need to set reference data in the LZX window
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ # Abstract base class for offset calculators
5
+ #
6
+ # Single responsibility: Calculate file positions within archive.
7
+ # Strategy pattern: Different formats implement different calculation strategies.
8
+ #
9
+ # Subclasses must implement:
10
+ # - calculate(structure) - Returns hash of offsets
11
+ #
12
+ # @example Creating a calculator
13
+ # class MyFormatCalculator < OffsetCalculator
14
+ # def calculate(structure)
15
+ # { header: 0, data: 100 }
16
+ # end
17
+ # end
18
+ class OffsetCalculator
19
+ # Calculate all offsets in archive structure
20
+ #
21
+ # @param structure [Hash] Archive structure with files, folders, etc.
22
+ # @return [Hash] Offset information
23
+ # @raise [NotImplementedError] if not implemented by subclass
24
+ def calculate(structure)
25
+ raise NotImplementedError,
26
+ "#{self.class.name} must implement calculate(structure)"
27
+ end
28
+
29
+ protected
30
+
31
+ # Helper: Calculate cumulative offsets for items
32
+ #
33
+ # @param items [Array] Items to calculate offsets for
34
+ # @param initial_offset [Integer] Starting offset
35
+ # @yield [item] Block that returns size for each item
36
+ # @return [Array<Hash>] Items with their offsets
37
+ def cumulative_offsets(items, initial_offset = 0)
38
+ offset = initial_offset
39
+ items.map do |item|
40
+ current_offset = offset
41
+ item_size = yield(item)
42
+ offset += item_size
43
+ { item: item, offset: current_offset, size: item_size }
44
+ end
45
+ end
46
+ end
47
+
48
+ # CAB-specific offset calculator
49
+ #
50
+ # Calculates offsets for CFHEADER, CFFOLDER entries, CFFILE entries,
51
+ # and CFDATA blocks in Microsoft Cabinet files.
52
+ class CABOffsetCalculator < OffsetCalculator
53
+ # Calculate CAB file offsets
54
+ #
55
+ # @param structure [Hash] Must contain :folders and :files
56
+ # @return [Hash] Offset information
57
+ def calculate(structure)
58
+ offset = Constants::CFHEADER_SIZE
59
+
60
+ # Folders section
61
+ folders_offset = offset
62
+ offset += Constants::CFFOLDER_SIZE * structure[:folders].size
63
+
64
+ # Files section
65
+ files_offset = offset
66
+ structure[:files].each do |file_info|
67
+ offset += Constants::CFFILE_SIZE
68
+ offset += file_info[:name].bytesize + 1 # null-terminated
69
+ end
70
+
71
+ # Data blocks section
72
+ data_offset = offset
73
+
74
+ {
75
+ folders: folders_offset,
76
+ files: files_offset,
77
+ data: data_offset,
78
+ }
79
+ end
80
+ end
81
+ end