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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +799 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE +29 -0
- data/README.adoc +1207 -0
- data/exe/cabriolet +6 -0
- data/lib/cabriolet/auto.rb +173 -0
- data/lib/cabriolet/binary/bitstream.rb +148 -0
- data/lib/cabriolet/binary/bitstream_writer.rb +180 -0
- data/lib/cabriolet/binary/chm_structures.rb +213 -0
- data/lib/cabriolet/binary/hlp_structures.rb +66 -0
- data/lib/cabriolet/binary/kwaj_structures.rb +74 -0
- data/lib/cabriolet/binary/lit_structures.rb +107 -0
- data/lib/cabriolet/binary/oab_structures.rb +112 -0
- data/lib/cabriolet/binary/structures.rb +56 -0
- data/lib/cabriolet/binary/szdd_structures.rb +60 -0
- data/lib/cabriolet/cab/compressor.rb +382 -0
- data/lib/cabriolet/cab/decompressor.rb +510 -0
- data/lib/cabriolet/cab/extractor.rb +357 -0
- data/lib/cabriolet/cab/parser.rb +264 -0
- data/lib/cabriolet/chm/compressor.rb +513 -0
- data/lib/cabriolet/chm/decompressor.rb +436 -0
- data/lib/cabriolet/chm/parser.rb +254 -0
- data/lib/cabriolet/cli.rb +776 -0
- data/lib/cabriolet/compressors/base.rb +34 -0
- data/lib/cabriolet/compressors/lzss.rb +250 -0
- data/lib/cabriolet/compressors/lzx.rb +581 -0
- data/lib/cabriolet/compressors/mszip.rb +315 -0
- data/lib/cabriolet/compressors/quantum.rb +446 -0
- data/lib/cabriolet/constants.rb +75 -0
- data/lib/cabriolet/decompressors/base.rb +39 -0
- data/lib/cabriolet/decompressors/lzss.rb +138 -0
- data/lib/cabriolet/decompressors/lzx.rb +726 -0
- data/lib/cabriolet/decompressors/mszip.rb +390 -0
- data/lib/cabriolet/decompressors/none.rb +27 -0
- data/lib/cabriolet/decompressors/quantum.rb +456 -0
- data/lib/cabriolet/errors.rb +39 -0
- data/lib/cabriolet/format_detector.rb +156 -0
- data/lib/cabriolet/hlp/compressor.rb +272 -0
- data/lib/cabriolet/hlp/decompressor.rb +198 -0
- data/lib/cabriolet/hlp/parser.rb +131 -0
- data/lib/cabriolet/huffman/decoder.rb +79 -0
- data/lib/cabriolet/huffman/encoder.rb +108 -0
- data/lib/cabriolet/huffman/tree.rb +138 -0
- data/lib/cabriolet/kwaj/compressor.rb +479 -0
- data/lib/cabriolet/kwaj/decompressor.rb +237 -0
- data/lib/cabriolet/kwaj/parser.rb +183 -0
- data/lib/cabriolet/lit/compressor.rb +255 -0
- data/lib/cabriolet/lit/decompressor.rb +250 -0
- data/lib/cabriolet/models/cabinet.rb +81 -0
- data/lib/cabriolet/models/chm_file.rb +28 -0
- data/lib/cabriolet/models/chm_header.rb +67 -0
- data/lib/cabriolet/models/chm_section.rb +38 -0
- data/lib/cabriolet/models/file.rb +119 -0
- data/lib/cabriolet/models/folder.rb +102 -0
- data/lib/cabriolet/models/folder_data.rb +21 -0
- data/lib/cabriolet/models/hlp_file.rb +45 -0
- data/lib/cabriolet/models/hlp_header.rb +37 -0
- data/lib/cabriolet/models/kwaj_header.rb +98 -0
- data/lib/cabriolet/models/lit_header.rb +55 -0
- data/lib/cabriolet/models/oab_header.rb +95 -0
- data/lib/cabriolet/models/szdd_header.rb +72 -0
- data/lib/cabriolet/modifier.rb +326 -0
- data/lib/cabriolet/oab/compressor.rb +353 -0
- data/lib/cabriolet/oab/decompressor.rb +315 -0
- data/lib/cabriolet/parallel.rb +333 -0
- data/lib/cabriolet/repairer.rb +288 -0
- data/lib/cabriolet/streaming.rb +221 -0
- data/lib/cabriolet/system/file_handle.rb +107 -0
- data/lib/cabriolet/system/io_system.rb +87 -0
- data/lib/cabriolet/system/memory_handle.rb +105 -0
- data/lib/cabriolet/szdd/compressor.rb +217 -0
- data/lib/cabriolet/szdd/decompressor.rb +184 -0
- data/lib/cabriolet/szdd/parser.rb +127 -0
- data/lib/cabriolet/validator.rb +332 -0
- data/lib/cabriolet/version.rb +5 -0
- data/lib/cabriolet.rb +104 -0
- 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
|