cabriolet 0.1.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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +799 -0
  3. data/CHANGELOG.md +44 -0
  4. data/LICENSE +29 -0
  5. data/README.adoc +1207 -0
  6. data/exe/cabriolet +6 -0
  7. data/lib/cabriolet/auto.rb +173 -0
  8. data/lib/cabriolet/binary/bitstream.rb +148 -0
  9. data/lib/cabriolet/binary/bitstream_writer.rb +180 -0
  10. data/lib/cabriolet/binary/chm_structures.rb +213 -0
  11. data/lib/cabriolet/binary/hlp_structures.rb +66 -0
  12. data/lib/cabriolet/binary/kwaj_structures.rb +74 -0
  13. data/lib/cabriolet/binary/lit_structures.rb +107 -0
  14. data/lib/cabriolet/binary/oab_structures.rb +112 -0
  15. data/lib/cabriolet/binary/structures.rb +56 -0
  16. data/lib/cabriolet/binary/szdd_structures.rb +60 -0
  17. data/lib/cabriolet/cab/compressor.rb +382 -0
  18. data/lib/cabriolet/cab/decompressor.rb +510 -0
  19. data/lib/cabriolet/cab/extractor.rb +357 -0
  20. data/lib/cabriolet/cab/parser.rb +264 -0
  21. data/lib/cabriolet/chm/compressor.rb +513 -0
  22. data/lib/cabriolet/chm/decompressor.rb +436 -0
  23. data/lib/cabriolet/chm/parser.rb +254 -0
  24. data/lib/cabriolet/cli.rb +776 -0
  25. data/lib/cabriolet/compressors/base.rb +34 -0
  26. data/lib/cabriolet/compressors/lzss.rb +250 -0
  27. data/lib/cabriolet/compressors/lzx.rb +581 -0
  28. data/lib/cabriolet/compressors/mszip.rb +315 -0
  29. data/lib/cabriolet/compressors/quantum.rb +446 -0
  30. data/lib/cabriolet/constants.rb +75 -0
  31. data/lib/cabriolet/decompressors/base.rb +39 -0
  32. data/lib/cabriolet/decompressors/lzss.rb +138 -0
  33. data/lib/cabriolet/decompressors/lzx.rb +726 -0
  34. data/lib/cabriolet/decompressors/mszip.rb +390 -0
  35. data/lib/cabriolet/decompressors/none.rb +27 -0
  36. data/lib/cabriolet/decompressors/quantum.rb +456 -0
  37. data/lib/cabriolet/errors.rb +39 -0
  38. data/lib/cabriolet/format_detector.rb +156 -0
  39. data/lib/cabriolet/hlp/compressor.rb +272 -0
  40. data/lib/cabriolet/hlp/decompressor.rb +198 -0
  41. data/lib/cabriolet/hlp/parser.rb +131 -0
  42. data/lib/cabriolet/huffman/decoder.rb +79 -0
  43. data/lib/cabriolet/huffman/encoder.rb +108 -0
  44. data/lib/cabriolet/huffman/tree.rb +138 -0
  45. data/lib/cabriolet/kwaj/compressor.rb +479 -0
  46. data/lib/cabriolet/kwaj/decompressor.rb +237 -0
  47. data/lib/cabriolet/kwaj/parser.rb +183 -0
  48. data/lib/cabriolet/lit/compressor.rb +255 -0
  49. data/lib/cabriolet/lit/decompressor.rb +250 -0
  50. data/lib/cabriolet/models/cabinet.rb +81 -0
  51. data/lib/cabriolet/models/chm_file.rb +28 -0
  52. data/lib/cabriolet/models/chm_header.rb +67 -0
  53. data/lib/cabriolet/models/chm_section.rb +38 -0
  54. data/lib/cabriolet/models/file.rb +119 -0
  55. data/lib/cabriolet/models/folder.rb +102 -0
  56. data/lib/cabriolet/models/folder_data.rb +21 -0
  57. data/lib/cabriolet/models/hlp_file.rb +45 -0
  58. data/lib/cabriolet/models/hlp_header.rb +37 -0
  59. data/lib/cabriolet/models/kwaj_header.rb +98 -0
  60. data/lib/cabriolet/models/lit_header.rb +55 -0
  61. data/lib/cabriolet/models/oab_header.rb +95 -0
  62. data/lib/cabriolet/models/szdd_header.rb +72 -0
  63. data/lib/cabriolet/modifier.rb +326 -0
  64. data/lib/cabriolet/oab/compressor.rb +353 -0
  65. data/lib/cabriolet/oab/decompressor.rb +315 -0
  66. data/lib/cabriolet/parallel.rb +333 -0
  67. data/lib/cabriolet/repairer.rb +288 -0
  68. data/lib/cabriolet/streaming.rb +221 -0
  69. data/lib/cabriolet/system/file_handle.rb +107 -0
  70. data/lib/cabriolet/system/io_system.rb +87 -0
  71. data/lib/cabriolet/system/memory_handle.rb +105 -0
  72. data/lib/cabriolet/szdd/compressor.rb +217 -0
  73. data/lib/cabriolet/szdd/decompressor.rb +184 -0
  74. data/lib/cabriolet/szdd/parser.rb +127 -0
  75. data/lib/cabriolet/validator.rb +332 -0
  76. data/lib/cabriolet/version.rb +5 -0
  77. data/lib/cabriolet.rb +104 -0
  78. metadata +157 -0
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module Cabriolet
6
+ module OAB
7
+ # Compressor for OAB (Outlook Offline Address Book) files
8
+ #
9
+ # OAB files use LZX compression. This compressor can create:
10
+ # - Full files (version 3.1): Complete address book data
11
+ # - Incremental patches (version 3.2): Binary patches (simplified)
12
+ #
13
+ # NOTE: This implementation is based on the OAB format specification
14
+ # derived from libmspack's decompressor. The original libmspack does not
15
+ # implement OAB compression. This is a best-effort implementation that
16
+ # may not produce files identical to Microsoft's OAB generator.
17
+ class Compressor
18
+ attr_reader :io_system
19
+ attr_accessor :buffer_size, :block_size
20
+
21
+ # Default buffer size for I/O operations
22
+ DEFAULT_BUFFER_SIZE = 4096
23
+
24
+ # Default block size (use 32KB like LZX frames)
25
+ DEFAULT_BLOCK_SIZE = 32_768
26
+
27
+ # OAB version numbers
28
+ VERSION_HI = 3
29
+ VERSION_LO_FULL = 1
30
+ VERSION_LO_PATCH = 2
31
+
32
+ # Initialize OAB compressor
33
+ #
34
+ # @param io_system [System::IOSystem, nil] I/O system or nil for default
35
+ def initialize(io_system = nil)
36
+ @io_system = io_system || System::IOSystem.new
37
+ @buffer_size = DEFAULT_BUFFER_SIZE
38
+ @block_size = DEFAULT_BLOCK_SIZE
39
+ end
40
+
41
+ # Compress a full OAB file
42
+ #
43
+ # @param input_file [String] Input file path
44
+ # @param output_file [String] Compressed OAB output path
45
+ # @param options [Hash] Compression options
46
+ # @option options [Integer] :block_size Block size (default: 32KB)
47
+ # @return [Integer] Bytes written
48
+ # @raise [Error] if compression fails
49
+ def compress(input_file, output_file, **options)
50
+ block_size = options.fetch(:block_size, @block_size)
51
+
52
+ input_handle = @io_system.open(input_file, Constants::MODE_READ)
53
+ output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
54
+
55
+ begin
56
+ # Get input size
57
+ input_size = @io_system.seek(input_handle, 0, Constants::SEEK_END)
58
+ @io_system.seek(input_handle, 0, Constants::SEEK_START)
59
+
60
+ # Write header
61
+ header = Binary::OABStructures::FullHeader.new
62
+ header.version_hi = VERSION_HI
63
+ header.version_lo = VERSION_LO_FULL
64
+ header.block_max = block_size
65
+ header.target_size = input_size
66
+
67
+ header_data = header.to_binary_s
68
+ bytes_written = @io_system.write(output_handle, header_data)
69
+
70
+ # Compress data in blocks
71
+ remaining = input_size
72
+ while remaining.positive?
73
+ block_bytes = compress_block(
74
+ input_handle, output_handle, block_size, remaining
75
+ )
76
+ bytes_written += block_bytes
77
+ remaining -= [block_size, remaining].min
78
+ end
79
+
80
+ bytes_written
81
+ ensure
82
+ @io_system.close(input_handle) if input_handle
83
+ @io_system.close(output_handle) if output_handle
84
+ end
85
+ end
86
+
87
+ # Compress data from memory to OAB format
88
+ #
89
+ # @param data [String] Input data to compress
90
+ # @param output_file [String] Compressed OAB output path
91
+ # @param options [Hash] Compression options
92
+ # @option options [Integer] :block_size Block size (default: 32KB)
93
+ # @return [Integer] Bytes written
94
+ # @raise [Error] if compression fails
95
+ def compress_data(data, output_file, **options)
96
+ block_size = options.fetch(:block_size, @block_size)
97
+
98
+ input_handle = System::MemoryHandle.new(data, Constants::MODE_READ)
99
+ output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
100
+
101
+ begin
102
+ # Write header
103
+ header = Binary::OABStructures::FullHeader.new
104
+ header.version_hi = VERSION_HI
105
+ header.version_lo = VERSION_LO_FULL
106
+ header.block_max = block_size
107
+ header.target_size = data.bytesize
108
+
109
+ header_data = header.to_binary_s
110
+ bytes_written = @io_system.write(output_handle, header_data)
111
+
112
+ # Compress data in blocks
113
+ remaining = data.bytesize
114
+ while remaining.positive?
115
+ block_bytes = compress_block(
116
+ input_handle, output_handle, block_size, remaining
117
+ )
118
+ bytes_written += block_bytes
119
+ remaining -= [block_size, remaining].min
120
+ end
121
+
122
+ bytes_written
123
+ ensure
124
+ @io_system.close(output_handle) if output_handle
125
+ end
126
+ end
127
+
128
+ # Create an incremental patch (simplified implementation)
129
+ #
130
+ # This is a simplified patch format that just stores the new data
131
+ # compressed. A full implementation would generate binary diffs.
132
+ #
133
+ # @param input_file [String] New version file path
134
+ # @param base_file [String] Base version file path
135
+ # @param output_file [String] Patch output path
136
+ # @param options [Hash] Compression options
137
+ # @option options [Integer] :block_size Block size (default: 32KB)
138
+ # @return [Integer] Bytes written
139
+ # @raise [Error] if compression fails
140
+ def compress_incremental(input_file, base_file, output_file, **options)
141
+ block_size = options.fetch(:block_size, @block_size)
142
+
143
+ # For now, just compress the new file with patch header
144
+ # A full implementation would generate binary diffs
145
+ input_handle = @io_system.open(input_file, Constants::MODE_READ)
146
+ base_handle = @io_system.open(base_file, Constants::MODE_READ)
147
+ output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
148
+
149
+ begin
150
+ # Get file sizes
151
+ input_size = @io_system.seek(input_handle, 0, Constants::SEEK_END)
152
+ @io_system.seek(input_handle, 0, Constants::SEEK_START)
153
+
154
+ base_size = @io_system.seek(base_handle, 0, Constants::SEEK_END)
155
+ @io_system.seek(base_handle, 0, Constants::SEEK_START)
156
+
157
+ # Read base data for CRC
158
+ base_data = @io_system.read(base_handle, base_size)
159
+ base_crc = Zlib.crc32(base_data)
160
+
161
+ # Read target data for CRC
162
+ target_data = @io_system.read(input_handle, input_size)
163
+ @io_system.seek(input_handle, 0, Constants::SEEK_START)
164
+ target_crc = Zlib.crc32(target_data)
165
+
166
+ # Write patch header
167
+ header = Binary::OABStructures::PatchHeader.new
168
+ header.version_hi = VERSION_HI
169
+ header.version_lo = VERSION_LO_PATCH
170
+ header.block_max = [block_size, 16].max
171
+ header.source_size = base_size
172
+ header.target_size = input_size
173
+ header.source_crc = base_crc
174
+ header.target_crc = target_crc
175
+
176
+ header_data = header.to_binary_s
177
+ bytes_written = @io_system.write(output_handle, header_data)
178
+
179
+ # Compress data in blocks (simplified - not true patches)
180
+ remaining = input_size
181
+ while remaining.positive?
182
+ block_bytes = compress_patch_block(
183
+ input_handle, output_handle, block_size, remaining, 0
184
+ )
185
+ bytes_written += block_bytes
186
+ remaining -= [block_size, remaining].min
187
+ end
188
+
189
+ bytes_written
190
+ ensure
191
+ @io_system.close(input_handle) if input_handle
192
+ @io_system.close(base_handle) if base_handle
193
+ @io_system.close(output_handle) if output_handle
194
+ end
195
+ end
196
+
197
+ private
198
+
199
+ # Compress a single block
200
+ #
201
+ # @param input_handle [System::FileHandle] Input file handle
202
+ # @param output_handle [System::FileHandle] Output file handle
203
+ # @param block_max [Integer] Maximum block size
204
+ # @param remaining [Integer] Remaining bytes
205
+ # @return [Integer] Bytes written
206
+ def compress_block(input_handle, output_handle, block_max, remaining)
207
+ # Read block data
208
+ block_size = [block_max, remaining].min
209
+ block_data = @io_system.read(input_handle, block_size)
210
+
211
+ if block_data.length < block_size
212
+ raise Error,
213
+ "Failed to read block data"
214
+ end
215
+
216
+ # Try LZX compression
217
+ compressed_data = compress_with_lzx(block_data)
218
+
219
+ # Use uncompressed if compression doesn't help
220
+ if compressed_data.nil? || compressed_data.bytesize >= block_data.bytesize
221
+ # Write uncompressed block
222
+ write_uncompressed_block(output_handle, block_data)
223
+ else
224
+ # Write compressed block
225
+ write_compressed_block(output_handle, block_data, compressed_data)
226
+ end
227
+ end
228
+
229
+ # Compress a single patch block (simplified)
230
+ #
231
+ # @param input_handle [System::FileHandle] Input file handle
232
+ # @param output_handle [System::FileHandle] Output file handle
233
+ # @param block_max [Integer] Maximum block size
234
+ # @param remaining [Integer] Remaining bytes
235
+ # @param source_size [Integer] Source block size (0 for simplified)
236
+ # @return [Integer] Bytes written
237
+ def compress_patch_block(input_handle, output_handle, block_max,
238
+ remaining, source_size)
239
+ # Read block data
240
+ block_size = [block_max, remaining].min
241
+ block_data = @io_system.read(input_handle, block_size)
242
+
243
+ if block_data.length < block_size
244
+ raise Error,
245
+ "Failed to read patch block data"
246
+ end
247
+
248
+ # Try LZX compression
249
+ compressed_data = compress_with_lzx(block_data)
250
+
251
+ # Use compressed data (or original if compression fails)
252
+ patch_data = compressed_data && compressed_data.bytesize < block_data.bytesize ? compressed_data : block_data
253
+ patch_size = patch_data.bytesize
254
+
255
+ # Calculate CRC
256
+ crc = Zlib.crc32(block_data)
257
+
258
+ # Write patch block header
259
+ block_header = Binary::OABStructures::PatchBlockHeader.new
260
+ block_header.patch_size = patch_size
261
+ block_header.target_size = block_size
262
+ block_header.source_size = source_size
263
+ block_header.crc = crc
264
+
265
+ header_data = block_header.to_binary_s
266
+ bytes_written = @io_system.write(output_handle, header_data)
267
+
268
+ # Write patch data
269
+ bytes_written += @io_system.write(output_handle, patch_data)
270
+
271
+ bytes_written
272
+ end
273
+
274
+ # Compress data using LZX
275
+ #
276
+ # @param data [String] Data to compress
277
+ # @return [String, nil] Compressed data or nil if compression failed
278
+ def compress_with_lzx(data)
279
+ return nil if data.empty?
280
+
281
+ # Calculate window bits for this block
282
+ window_bits = 17
283
+ window_bits += 1 while window_bits < 25 && (1 << window_bits) < data.bytesize
284
+
285
+ # Create memory handles
286
+ input_mem = System::MemoryHandle.new(data, Constants::MODE_READ)
287
+ output_mem = System::MemoryHandle.new("", Constants::MODE_WRITE)
288
+
289
+ # Compress with LZX
290
+ compressor = Compressors::LZX.new(
291
+ @io_system, input_mem, output_mem, @buffer_size,
292
+ window_bits: window_bits
293
+ )
294
+
295
+ compressor.compress
296
+
297
+ output_mem.data
298
+ rescue StandardError => e
299
+ warn "[Cabriolet] LZX compression failed: #{e.message}" if Cabriolet.verbose
300
+ nil
301
+ end
302
+
303
+ # Write uncompressed block
304
+ #
305
+ # @param output_handle [System::FileHandle] Output file handle
306
+ # @param block_data [String] Block data
307
+ # @return [Integer] Bytes written
308
+ def write_uncompressed_block(output_handle, block_data)
309
+ crc = Zlib.crc32(block_data)
310
+
311
+ # Write block header
312
+ block_header = Binary::OABStructures::BlockHeader.new
313
+ block_header.flags = 0 # Uncompressed
314
+ block_header.compressed_size = block_data.bytesize
315
+ block_header.uncompressed_size = block_data.bytesize
316
+ block_header.crc = crc
317
+
318
+ header_data = block_header.to_binary_s
319
+ bytes_written = @io_system.write(output_handle, header_data)
320
+
321
+ # Write block data
322
+ bytes_written += @io_system.write(output_handle, block_data)
323
+
324
+ bytes_written
325
+ end
326
+
327
+ # Write compressed block
328
+ #
329
+ # @param output_handle [System::FileHandle] Output file handle
330
+ # @param original_data [String] Original uncompressed data
331
+ # @param compressed_data [String] Compressed data
332
+ # @return [Integer] Bytes written
333
+ def write_compressed_block(output_handle, original_data, compressed_data)
334
+ crc = Zlib.crc32(original_data)
335
+
336
+ # Write block header
337
+ block_header = Binary::OABStructures::BlockHeader.new
338
+ block_header.flags = 1 # LZX compressed
339
+ block_header.compressed_size = compressed_data.bytesize
340
+ block_header.uncompressed_size = original_data.bytesize
341
+ block_header.crc = crc
342
+
343
+ header_data = block_header.to_binary_s
344
+ bytes_written = @io_system.write(output_handle, header_data)
345
+
346
+ # Write compressed data
347
+ bytes_written += @io_system.write(output_handle, compressed_data)
348
+
349
+ bytes_written
350
+ end
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module Cabriolet
6
+ module OAB
7
+ # Decompressor for OAB (Outlook Offline Address Book) files
8
+ #
9
+ # OAB files use LZX compression and come in two formats:
10
+ # - Full files (version 3.1): Complete address book data
11
+ # - Incremental patches (version 3.2): Binary patches applied to base file
12
+ #
13
+ # This implementation is based on libmspack's oabd.c
14
+ #
15
+ # NOTE: This implementation cannot be fully validated due to lack of test
16
+ # fixtures. OAB files are specialized Outlook data files. Testing relies
17
+ # on round-trip compression/decompression.
18
+ class Decompressor
19
+ attr_reader :io_system
20
+ attr_accessor :buffer_size
21
+
22
+ # Default buffer size for I/O operations
23
+ DEFAULT_BUFFER_SIZE = 4096
24
+
25
+ # Initialize OAB decompressor
26
+ #
27
+ # @param io_system [System::IOSystem, nil] I/O system or nil for default
28
+ def initialize(io_system = nil)
29
+ @io_system = io_system || System::IOSystem.new
30
+ @buffer_size = DEFAULT_BUFFER_SIZE
31
+ end
32
+
33
+ # Decompress a full OAB file
34
+ #
35
+ # @param input_file [String] Compressed OAB file path
36
+ # @param output_file [String] Decompressed output path
37
+ # @return [Integer] Bytes written
38
+ # @raise [Error] if decompression fails
39
+ def decompress(input_file, output_file)
40
+ input_handle = @io_system.open(input_file, Constants::MODE_READ)
41
+ output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
42
+
43
+ begin
44
+ # Read and validate header
45
+ header_data = @io_system.read(input_handle, 16)
46
+ raise Error, "Failed to read OAB header" if header_data.length < 16
47
+
48
+ header = Binary::OABStructures::FullHeader.read(header_data)
49
+ raise Error, "Invalid OAB header" unless header.valid?
50
+
51
+ block_max = header.block_max
52
+ target_size = header.target_size
53
+ total_written = 0
54
+
55
+ # Process blocks until target size reached
56
+ while target_size.positive?
57
+ total_written += decompress_block(
58
+ input_handle, output_handle, block_max, target_size
59
+ )
60
+ target_size -= [block_max, target_size].min
61
+ end
62
+
63
+ total_written
64
+ ensure
65
+ @io_system.close(input_handle) if input_handle
66
+ @io_system.close(output_handle) if output_handle
67
+ end
68
+ end
69
+
70
+ # Decompress an incremental patch file
71
+ #
72
+ # @param patch_file [String] Compressed patch file path
73
+ # @param base_file [String] Base (uncompressed) file path
74
+ # @param output_file [String] Output file path
75
+ # @return [Integer] Bytes written
76
+ # @raise [Error] if decompression fails
77
+ def decompress_incremental(patch_file, base_file, output_file)
78
+ patch_handle = @io_system.open(patch_file, Constants::MODE_READ)
79
+ base_handle = @io_system.open(base_file, Constants::MODE_READ)
80
+ output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
81
+
82
+ begin
83
+ # Read and validate patch header
84
+ header_data = @io_system.read(patch_handle, 28)
85
+ if header_data.length < 28
86
+ raise Error,
87
+ "Failed to read OAB patch header"
88
+ end
89
+
90
+ header = Binary::OABStructures::PatchHeader.read(header_data)
91
+ raise Error, "Invalid OAB patch header" unless header.valid?
92
+
93
+ block_max = [header.block_max, 16].max # At least 16 for header
94
+ target_size = header.target_size
95
+ total_written = 0
96
+
97
+ # Process patch blocks until target size reached
98
+ while target_size.positive?
99
+ total_written += decompress_patch_block(
100
+ patch_handle, base_handle, output_handle, block_max, target_size
101
+ )
102
+ target_size = header.target_size - total_written
103
+ end
104
+
105
+ total_written
106
+ ensure
107
+ @io_system.close(patch_handle) if patch_handle
108
+ @io_system.close(base_handle) if base_handle
109
+ @io_system.close(output_handle) if output_handle
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ # Decompress a single OAB block (full file)
116
+ #
117
+ # @param input_handle [System::FileHandle] Input file handle
118
+ # @param output_handle [System::FileHandle] Output file handle
119
+ # @param block_max [Integer] Maximum block size
120
+ # @param target_remaining [Integer] Remaining bytes to decompress
121
+ # @return [Integer] Bytes written
122
+ def decompress_block(input_handle, output_handle, block_max,
123
+ target_remaining)
124
+ # Read block header
125
+ block_data = @io_system.read(input_handle, 16)
126
+ raise Error, "Failed to read block header" if block_data.length < 16
127
+
128
+ block_header = Binary::OABStructures::BlockHeader.read(block_data)
129
+
130
+ # Validate block
131
+ if block_header.uncompressed_size > block_max ||
132
+ block_header.uncompressed_size > target_remaining ||
133
+ block_header.flags > 1
134
+ raise Error, "Invalid block header"
135
+ end
136
+
137
+ if block_header.uncompressed?
138
+ # Uncompressed block
139
+ if block_header.uncompressed_size != block_header.compressed_size
140
+ raise Error, "Uncompressed block size mismatch"
141
+ end
142
+
143
+ decompress_uncompressed_block(
144
+ input_handle, output_handle, block_header.uncompressed_size
145
+ )
146
+ else
147
+ # LZX compressed block
148
+ decompress_lzx_block(
149
+ input_handle, output_handle, block_header
150
+ )
151
+ end
152
+ end
153
+
154
+ # Decompress a single patch block (incremental)
155
+ #
156
+ # @param patch_handle [System::FileHandle] Patch file handle
157
+ # @param base_handle [System::FileHandle] Base file handle
158
+ # @param output_handle [System::FileHandle] Output file handle
159
+ # @param block_max [Integer] Maximum block size
160
+ # @param target_remaining [Integer] Remaining bytes to decompress
161
+ # @return [Integer] Bytes written
162
+ def decompress_patch_block(patch_handle, base_handle, output_handle,
163
+ block_max, target_remaining)
164
+ # Read patch block header
165
+ block_data = @io_system.read(patch_handle, 16)
166
+ if block_data.length < 16
167
+ raise Error,
168
+ "Failed to read patch block header"
169
+ end
170
+
171
+ block_header = Binary::OABStructures::PatchBlockHeader.read(block_data)
172
+
173
+ # Validate block
174
+ if block_header.target_size > block_max ||
175
+ block_header.target_size > target_remaining ||
176
+ block_header.source_size > block_max
177
+ raise Error, "Invalid patch block header"
178
+ end
179
+
180
+ # Calculate window size for LZX
181
+ window_size = ((block_header.source_size + 32_767) & ~32_767) +
182
+ block_header.target_size
183
+ window_bits = 17
184
+
185
+ window_bits += 1 while window_bits < 25 && (1 << window_bits) < window_size
186
+
187
+ # Read reference data from base file
188
+ reference_data = @io_system.read(base_handle, block_header.source_size)
189
+
190
+ # Decompress patch with LZX using reference data
191
+ decompress_lzx_patch_block(
192
+ patch_handle, output_handle, block_header, window_bits, reference_data
193
+ )
194
+ end
195
+
196
+ # Decompress uncompressed block
197
+ #
198
+ # @param input_handle [System::FileHandle] Input file handle
199
+ # @param output_handle [System::FileHandle] Output file handle
200
+ # @param size [Integer] Block size
201
+ # @return [Integer] Bytes written
202
+ def decompress_uncompressed_block(input_handle, output_handle, size)
203
+ bytes_written = 0
204
+
205
+ while size.positive?
206
+ chunk_size = [@buffer_size, size].min
207
+ data = @io_system.read(input_handle, chunk_size)
208
+
209
+ if data.length < chunk_size
210
+ raise Error,
211
+ "Failed to read uncompressed data"
212
+ end
213
+
214
+ @io_system.write(output_handle, data)
215
+ bytes_written += chunk_size
216
+ size -= chunk_size
217
+ end
218
+
219
+ bytes_written
220
+ end
221
+
222
+ # Decompress LZX compressed block
223
+ #
224
+ # @param input_handle [System::FileHandle] Input file handle
225
+ # @param output_handle [System::FileHandle] Output file handle
226
+ # @param block_header [Binary::OABStructures::BlockHeader] Block header
227
+ # @return [Integer] Bytes written
228
+ def decompress_lzx_block(input_handle, output_handle, block_header)
229
+ # Calculate window bits for LZX
230
+ window_bits = 17
231
+ block_size = block_header.uncompressed_size
232
+
233
+ window_bits += 1 while window_bits < 25 && (1 << window_bits) < block_size
234
+
235
+ # Read compressed data
236
+ compressed_data = @io_system.read(input_handle,
237
+ block_header.compressed_size)
238
+ if compressed_data.length < block_header.compressed_size
239
+ raise Error,
240
+ "Failed to read compressed data"
241
+ end
242
+
243
+ # Create memory handles for LZX decompression
244
+ input_mem = System::MemoryHandle.new(compressed_data, Constants::MODE_READ)
245
+ output_mem = System::MemoryHandle.new("", Constants::MODE_WRITE)
246
+
247
+ # Decompress with LZX
248
+ lzx = Decompressors::LZX.new(
249
+ @io_system, input_mem, output_mem, @buffer_size,
250
+ window_bits: window_bits,
251
+ reset_interval: 0,
252
+ output_length: block_size,
253
+ is_delta: false
254
+ )
255
+
256
+ bytes_decompressed = lzx.decompress(block_size)
257
+
258
+ # Verify CRC
259
+ actual_crc = Zlib.crc32(output_mem.data)
260
+ raise Error, "CRC mismatch in block" if actual_crc != block_header.crc
261
+
262
+ # Write decompressed data
263
+ @io_system.write(output_handle, output_mem.data)
264
+ bytes_decompressed
265
+ end
266
+
267
+ # Decompress LZX patch block with reference data
268
+ #
269
+ # @param patch_handle [System::FileHandle] Patch file handle
270
+ # @param output_handle [System::FileHandle] Output file handle
271
+ # @param block_header [Binary::OABStructures::PatchBlockHeader] Block header
272
+ # @param window_bits [Integer] LZX window bits
273
+ # @param reference_data [String] Reference data from base file
274
+ # @return [Integer] Bytes written
275
+ def decompress_lzx_patch_block(patch_handle, output_handle, block_header,
276
+ window_bits, _reference_data)
277
+ # Read compressed patch data
278
+ compressed_data = @io_system.read(patch_handle, block_header.patch_size)
279
+ if compressed_data.length < block_header.patch_size
280
+ raise Error,
281
+ "Failed to read patch data"
282
+ end
283
+
284
+ # Create memory handles for LZX decompression
285
+ input_mem = System::MemoryHandle.new(compressed_data, Constants::MODE_READ)
286
+ output_mem = System::MemoryHandle.new("", Constants::MODE_WRITE)
287
+
288
+ # Decompress with LZX DELTA (includes reference data)
289
+ lzx = Decompressors::LZX.new(
290
+ @io_system, input_mem, output_mem, @buffer_size,
291
+ window_bits: window_bits,
292
+ reset_interval: 0,
293
+ output_length: block_header.target_size,
294
+ is_delta: true
295
+ )
296
+
297
+ # For patches, we'd need to set reference data in the LZX window
298
+ # This is a simplified implementation - full support would require
299
+ # extending the LZX decompressor to handle reference data
300
+ bytes_decompressed = lzx.decompress(block_header.target_size)
301
+
302
+ # Verify CRC
303
+ actual_crc = Zlib.crc32(output_mem.data)
304
+ if actual_crc != block_header.crc
305
+ raise Error,
306
+ "CRC mismatch in patch block"
307
+ end
308
+
309
+ # Write decompressed data
310
+ @io_system.write(output_handle, output_mem.data)
311
+ bytes_decompressed
312
+ end
313
+ end
314
+ end
315
+ end