cabriolet 0.2.0 → 0.2.1
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 +4 -4
- data/README.adoc +3 -0
- data/lib/cabriolet/binary/bitstream.rb +32 -21
- data/lib/cabriolet/binary/bitstream_writer.rb +21 -4
- data/lib/cabriolet/cab/compressor.rb +85 -53
- data/lib/cabriolet/cab/decompressor.rb +2 -1
- data/lib/cabriolet/cab/extractor.rb +2 -35
- data/lib/cabriolet/cab/file_compression_work.rb +52 -0
- data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
- data/lib/cabriolet/checksum.rb +49 -0
- data/lib/cabriolet/collections/file_collection.rb +175 -0
- data/lib/cabriolet/compressors/quantum.rb +3 -51
- data/lib/cabriolet/decompressors/quantum.rb +81 -52
- data/lib/cabriolet/extraction/base_extractor.rb +88 -0
- data/lib/cabriolet/extraction/extractor.rb +171 -0
- data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
- data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
- data/lib/cabriolet/format_base.rb +79 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +28 -503
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
- data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
- data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
- data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
- data/lib/cabriolet/huffman/encoder.rb +15 -12
- data/lib/cabriolet/lit/compressor.rb +45 -689
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/directory_builder.rb +153 -0
- data/lib/cabriolet/lit/guid_generator.rb +16 -0
- data/lib/cabriolet/lit/header_writer.rb +124 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -0
- data/lib/cabriolet/quantum_shared.rb +105 -0
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +114 -3
- metadata +38 -4
- data/lib/cabriolet/auto.rb +0 -173
- data/lib/cabriolet/parallel.rb +0 -333
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d301c701aa57d4033c110e9610d76f11e26ae3b8debd3a52d8a3df5ccb725905
|
|
4
|
+
data.tar.gz: d009c4741e7885ec25ad6766dd79158d1955b80ffc16e9acb0c5a0c1a813b9d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd0314bf6196cad4749d2497ee639b7a355eab2a93a4a5e4967878e64cabd6413c08bbfab99aa767e67d1a13a0a3e7e428801819192c6506dba2dbf30f653b3d
|
|
7
|
+
data.tar.gz: dd1bb57af42a9cc7fa8556bd05632ce79b3b2a994d8276bee8349db7f10428e9d61f254ea8f928583dc8e7f0eca8e2bdd326fd137912fe5afc74c1b0b3f684da
|
data/README.adoc
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
image:https://img.shields.io/gem/v/cabriolet.svg[RubyGems Version, link=https://rubygems.org/gems/cabriolet]
|
|
4
4
|
image:https://img.shields.io/github/license/omnizip/cabriolet.svg[License]
|
|
5
5
|
|
|
6
|
+
image:https://img.shields.io/badge/Website-Cabriolet_documentation-blue.svg["Documentation site", link="https://omnizip.github.io/cabriolet"]
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
Pure Ruby implementation for extracting and creating Microsoft compression
|
|
7
10
|
format files.
|
|
8
11
|
|
|
@@ -51,6 +51,24 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
|
|
|
51
51
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
|
+
# Read 2 bytes as a little-endian 16-bit word for MSB mode
|
|
55
|
+
# This is a shared helper for read_bits_msb and peek_bits
|
|
56
|
+
#
|
|
57
|
+
# @return [Integer] 16-bit word, or nil if at EOF and not in salvage mode
|
|
58
|
+
def read_msb_word
|
|
59
|
+
byte0 = read_byte
|
|
60
|
+
if byte0.nil? && (@salvage || @input_end)
|
|
61
|
+
byte0 = 0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
byte1 = read_byte
|
|
65
|
+
if byte1.nil?
|
|
66
|
+
byte1 = 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
byte0 | (byte1 << 8)
|
|
70
|
+
end
|
|
71
|
+
|
|
54
72
|
# Read bits in LSB-first order
|
|
55
73
|
#
|
|
56
74
|
# Per libmspack: EOF handling allows padding to avoid bitstream overrun.
|
|
@@ -95,34 +113,17 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
|
|
|
95
113
|
def read_bits_msb(num_bits)
|
|
96
114
|
# Ensure we have enough bits in the buffer
|
|
97
115
|
while @bits_left < num_bits
|
|
98
|
-
|
|
99
|
-
byte0 = read_byte
|
|
100
|
-
if byte0.nil? && (@salvage || @input_end)
|
|
101
|
-
# First EOF: pad with zeros
|
|
102
|
-
# Second EOF: read_byte will raise DecompressionError
|
|
103
|
-
byte0 = 0
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
byte1 = read_byte
|
|
107
|
-
if byte1.nil?
|
|
108
|
-
# Pad with 0 if only 1 byte left (or EOF)
|
|
109
|
-
byte1 = 0
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Combine as little-endian 16-bit value
|
|
113
|
-
word = byte0 | (byte1 << 8)
|
|
116
|
+
word = read_msb_word
|
|
114
117
|
|
|
115
118
|
# DEBUG
|
|
116
|
-
warn "DEBUG MSB read_bytes:
|
|
119
|
+
warn "DEBUG MSB read_bytes: word=0x#{word.to_s(16)} bits_left=#{@bits_left}" if ENV["DEBUG_BITSTREAM"]
|
|
117
120
|
|
|
118
121
|
# INJECT_BITS (MSB): inject at the left side
|
|
119
|
-
# bit_buffer |= word << (BITBUF_WIDTH -16 - bits_left)
|
|
120
122
|
@bit_buffer |= (word << (@bitbuf_width - 16 - @bits_left))
|
|
121
123
|
@bits_left += 16
|
|
122
124
|
end
|
|
123
125
|
|
|
124
126
|
# PEEK_BITS (MSB): extract from the left
|
|
125
|
-
# result = bit_buffer >> (BITBUF_WIDTH - num_bits)
|
|
126
127
|
result = @bit_buffer >> (@bitbuf_width - num_bits)
|
|
127
128
|
|
|
128
129
|
# REMOVE_BITS (MSB): shift left
|
|
@@ -251,9 +252,19 @@ buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
|
|
|
251
252
|
# @return [Integer] Bits as an integer
|
|
252
253
|
def read_bits_be(num_bits)
|
|
253
254
|
result = 0
|
|
254
|
-
num_bits
|
|
255
|
-
|
|
255
|
+
full_bytes = num_bits / 8
|
|
256
|
+
remaining_bits = num_bits % 8
|
|
257
|
+
|
|
258
|
+
# Read full bytes first (more efficient than bit-by-bit)
|
|
259
|
+
full_bytes.times do
|
|
260
|
+
result = (result << 8) | read_bits(8)
|
|
256
261
|
end
|
|
262
|
+
|
|
263
|
+
# Read remaining bits
|
|
264
|
+
if remaining_bits.positive?
|
|
265
|
+
result = (result << remaining_bits) | read_bits(remaining_bits)
|
|
266
|
+
end
|
|
267
|
+
|
|
257
268
|
result
|
|
258
269
|
end
|
|
259
270
|
|
|
@@ -4,6 +4,10 @@ module Cabriolet
|
|
|
4
4
|
module Binary
|
|
5
5
|
# BitstreamWriter provides bit-level I/O operations for writing compressed data
|
|
6
6
|
class BitstreamWriter
|
|
7
|
+
# Pre-computed byte constants for fast single-byte writes
|
|
8
|
+
# Avoids repeated array packing for each byte written
|
|
9
|
+
BYTE_CONSTANTS = Array.new(256) { |i| [i].pack("C") }.freeze
|
|
10
|
+
|
|
7
11
|
attr_reader :io_system, :handle, :buffer_size
|
|
8
12
|
|
|
9
13
|
# Initialize a new bitstream writer
|
|
@@ -129,7 +133,8 @@ module Cabriolet
|
|
|
129
133
|
# @param byte [Integer] Byte value to write
|
|
130
134
|
# @return [void]
|
|
131
135
|
def write_byte(byte)
|
|
132
|
-
|
|
136
|
+
# Use pre-encoded byte constant for better performance
|
|
137
|
+
data = BYTE_CONSTANTS[byte]
|
|
133
138
|
# DEBUG
|
|
134
139
|
if ENV["DEBUG_BITSTREAM"]
|
|
135
140
|
warn "DEBUG write_byte: pos=#{@bits_in_buffer} byte=#{byte} (#{byte.to_s(2).rjust(
|
|
@@ -217,9 +222,21 @@ module Cabriolet
|
|
|
217
222
|
# @param num_bits [Integer] Number of bits to write
|
|
218
223
|
# @return [void]
|
|
219
224
|
def write_bits_be(value, num_bits)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
# Write full bytes first for better performance
|
|
226
|
+
full_bytes = num_bits / 8
|
|
227
|
+
remaining_bits = num_bits % 8
|
|
228
|
+
|
|
229
|
+
# Write complete bytes MSB first
|
|
230
|
+
full_bytes.times do |i|
|
|
231
|
+
byte_shift = num_bits - 8 - (i * 8)
|
|
232
|
+
byte = (value >> byte_shift) & 0xFF
|
|
233
|
+
write_bits(byte, 8)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Write remaining bits
|
|
237
|
+
if remaining_bits.positive?
|
|
238
|
+
remaining_value = value & ((1 << remaining_bits) - 1)
|
|
239
|
+
write_bits(remaining_value, remaining_bits)
|
|
223
240
|
end
|
|
224
241
|
end
|
|
225
242
|
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../checksum"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
|
|
3
6
|
module Cabriolet
|
|
4
7
|
module CAB
|
|
5
8
|
# Compressor creates CAB files from source files
|
|
6
9
|
# rubocop:disable Metrics/ClassLength
|
|
7
10
|
class Compressor
|
|
8
|
-
attr_reader :io_system, :files, :compression, :set_id, :cabinet_index
|
|
11
|
+
attr_reader :io_system, :files, :compression, :set_id, :cabinet_index,
|
|
12
|
+
:workers
|
|
9
13
|
|
|
10
14
|
# Initialize a new compressor
|
|
11
15
|
#
|
|
12
16
|
# @param io_system [System::IOSystem] I/O system for writing
|
|
13
17
|
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
14
|
-
|
|
18
|
+
# @param workers [Integer] Number of parallel worker threads (default: 1 for sequential)
|
|
19
|
+
def initialize(io_system = nil, algorithm_factory = nil, workers: 1)
|
|
15
20
|
@io_system = io_system || System::IOSystem.new
|
|
16
21
|
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
17
22
|
@files = []
|
|
18
23
|
@compression = :mszip
|
|
19
24
|
@set_id = rand(0xFFFF)
|
|
20
25
|
@cabinet_index = 0
|
|
26
|
+
@workers = workers
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
# Add a file to the cabinet
|
|
@@ -56,6 +62,9 @@ module Cabriolet
|
|
|
56
62
|
@set_id = options[:set_id] || @set_id
|
|
57
63
|
@cabinet_index = options[:cabinet_index] || @cabinet_index
|
|
58
64
|
|
|
65
|
+
# Validate and cache compression method value to avoid repeated hash lookups
|
|
66
|
+
@compression_method = compression_type_value
|
|
67
|
+
|
|
59
68
|
# Collect file information
|
|
60
69
|
file_infos = collect_file_infos
|
|
61
70
|
|
|
@@ -129,17 +138,80 @@ module Cabriolet
|
|
|
129
138
|
|
|
130
139
|
# Compress all files and return block data
|
|
131
140
|
def compress_files(file_infos)
|
|
141
|
+
return compress_files_sequential(file_infos) if @workers <= 1
|
|
142
|
+
|
|
143
|
+
compress_files_parallel(file_infos)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Compress files using parallel workers via Fractor
|
|
147
|
+
def compress_files_parallel(file_infos)
|
|
148
|
+
require_relative "file_compression_work"
|
|
149
|
+
require_relative "file_compression_worker"
|
|
150
|
+
|
|
151
|
+
compression_method = @compression_method || compression_type_value
|
|
152
|
+
|
|
153
|
+
# Create work items for each file
|
|
154
|
+
work_items = file_infos.map do |info|
|
|
155
|
+
FileCompressionWork.new(
|
|
156
|
+
source_path: info[:source_path],
|
|
157
|
+
compression_method: compression_method,
|
|
158
|
+
block_size: Constants::BLOCK_MAX,
|
|
159
|
+
io_system: @io_system,
|
|
160
|
+
algorithm_factory: @algorithm_factory,
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Create worker pool
|
|
165
|
+
worker_pool = Fractor::WorkerPool.new(
|
|
166
|
+
FileCompressionWorker,
|
|
167
|
+
num_workers: @workers,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Submit all work items and wait for completion
|
|
171
|
+
results = worker_pool.process_work(work_items)
|
|
172
|
+
|
|
173
|
+
# Aggregate results in original order
|
|
174
|
+
file_result_map = {}
|
|
175
|
+
total_uncompressed = 0
|
|
176
|
+
all_blocks = []
|
|
177
|
+
|
|
178
|
+
results.each do |result|
|
|
179
|
+
if result.error
|
|
180
|
+
raise DecompressionError,
|
|
181
|
+
"Failed to compress #{result.error[:source_path]}: #{result.error[:message]}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
file_result_map[result.result[:source_path]] = result.result
|
|
185
|
+
total_uncompressed += result.result[:total_uncompressed]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Reorder blocks to match original file order
|
|
189
|
+
file_infos.each do |info|
|
|
190
|
+
file_result = file_result_map[info[:source_path]]
|
|
191
|
+
all_blocks.concat(file_result[:blocks])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
{
|
|
195
|
+
blocks: all_blocks,
|
|
196
|
+
total_uncompressed: total_uncompressed,
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Compress files sequentially (original implementation)
|
|
201
|
+
def compress_files_sequential(file_infos)
|
|
132
202
|
blocks = []
|
|
133
203
|
total_uncompressed = 0
|
|
134
204
|
|
|
135
205
|
file_infos.each do |info|
|
|
136
206
|
file_data = ::File.binread(info[:source_path])
|
|
137
|
-
|
|
207
|
+
file_size = file_data.bytesize
|
|
208
|
+
total_uncompressed += file_size
|
|
138
209
|
|
|
139
210
|
# Split into blocks of max 32KB
|
|
140
211
|
offset = 0
|
|
141
|
-
while offset <
|
|
142
|
-
|
|
212
|
+
while offset < file_size
|
|
213
|
+
remaining = file_size - offset
|
|
214
|
+
chunk_size = [Constants::BLOCK_MAX, remaining].min
|
|
143
215
|
chunk = file_data[offset, chunk_size]
|
|
144
216
|
|
|
145
217
|
# Compress chunk
|
|
@@ -169,18 +241,9 @@ module Cabriolet
|
|
|
169
241
|
input = System::MemoryHandle.new(data)
|
|
170
242
|
output = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
171
243
|
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
none: Constants::COMP_TYPE_NONE,
|
|
176
|
-
mszip: Constants::COMP_TYPE_MSZIP,
|
|
177
|
-
lzx: Constants::COMP_TYPE_LZX,
|
|
178
|
-
quantum: Constants::COMP_TYPE_QUANTUM,
|
|
179
|
-
}.fetch(@compression)
|
|
180
|
-
rescue KeyError
|
|
181
|
-
raise ArgumentError,
|
|
182
|
-
"Unsupported compression type: #{@compression}"
|
|
183
|
-
end
|
|
244
|
+
# Use cached compression method value (calculated in generate)
|
|
245
|
+
# Fallback to calculation if not yet cached
|
|
246
|
+
compression_method = @compression_method || compression_type_value
|
|
184
247
|
|
|
185
248
|
# Determine window bits based on compression type
|
|
186
249
|
window_bits = case @compression
|
|
@@ -278,7 +341,10 @@ cabinet_size)
|
|
|
278
341
|
mszip: Constants::COMP_TYPE_MSZIP,
|
|
279
342
|
lzx: Constants::COMP_TYPE_LZX,
|
|
280
343
|
quantum: Constants::COMP_TYPE_QUANTUM,
|
|
281
|
-
}.fetch(@compression
|
|
344
|
+
}.fetch(@compression) do
|
|
345
|
+
raise ArgumentError,
|
|
346
|
+
"Unsupported compression type: #{@compression}"
|
|
347
|
+
end
|
|
282
348
|
end
|
|
283
349
|
|
|
284
350
|
# Write CFFILE entry
|
|
@@ -331,41 +397,7 @@ cabinet_size)
|
|
|
331
397
|
# Same algorithm as used in Extractor
|
|
332
398
|
# rubocop:disable Metrics/MethodLength
|
|
333
399
|
def calculate_checksum(data, initial = 0)
|
|
334
|
-
|
|
335
|
-
bytes = data.bytes
|
|
336
|
-
|
|
337
|
-
# Process 4-byte chunks
|
|
338
|
-
(bytes.size / 4).times do |i|
|
|
339
|
-
offset = i * 4
|
|
340
|
-
value = bytes[offset] |
|
|
341
|
-
(bytes[offset + 1] << 8) |
|
|
342
|
-
(bytes[offset + 2] << 16) |
|
|
343
|
-
(bytes[offset + 3] << 24)
|
|
344
|
-
cksum ^= value
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
# Process remaining bytes
|
|
348
|
-
remainder = bytes.size % 4
|
|
349
|
-
if remainder.positive?
|
|
350
|
-
ul = 0
|
|
351
|
-
offset = bytes.size - remainder
|
|
352
|
-
|
|
353
|
-
case remainder
|
|
354
|
-
when 3
|
|
355
|
-
ul |= bytes[offset + 2] << 16
|
|
356
|
-
ul |= bytes[offset + 1] << 8
|
|
357
|
-
ul |= bytes[offset]
|
|
358
|
-
when 2
|
|
359
|
-
ul |= bytes[offset + 1] << 8
|
|
360
|
-
ul |= bytes[offset]
|
|
361
|
-
when 1
|
|
362
|
-
ul |= bytes[offset]
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
cksum ^= ul
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
cksum & 0xFFFFFFFF
|
|
400
|
+
Checksum.calculate(data, initial)
|
|
369
401
|
end
|
|
370
402
|
# rubocop:enable Metrics/MethodLength
|
|
371
403
|
end
|
|
@@ -97,7 +97,8 @@ module Cabriolet
|
|
|
97
97
|
# @param filename [String] Path to file to search
|
|
98
98
|
# @return [Models::Cabinet, nil] First cabinet found, or nil if none found
|
|
99
99
|
def search(filename)
|
|
100
|
-
|
|
100
|
+
# Reuse search buffer across searches for better performance
|
|
101
|
+
search_buf = @search_buffer ||= Array.new(@search_buffer_size)
|
|
101
102
|
first_cabinet = nil
|
|
102
103
|
link_cabinet = nil
|
|
103
104
|
first_len = 0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "../checksum"
|
|
4
5
|
|
|
5
6
|
module Cabriolet
|
|
6
7
|
module CAB
|
|
@@ -424,41 +425,7 @@ module Cabriolet
|
|
|
424
425
|
end
|
|
425
426
|
|
|
426
427
|
def calculate_checksum(data, initial = 0)
|
|
427
|
-
|
|
428
|
-
bytes = data.bytes
|
|
429
|
-
|
|
430
|
-
# Process 4-byte chunks
|
|
431
|
-
(bytes.size / 4).times do |i|
|
|
432
|
-
offset = i * 4
|
|
433
|
-
value = bytes[offset] |
|
|
434
|
-
(bytes[offset + 1] << 8) |
|
|
435
|
-
(bytes[offset + 2] << 16) |
|
|
436
|
-
(bytes[offset + 3] << 24)
|
|
437
|
-
cksum ^= value
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
# Process remaining bytes
|
|
441
|
-
remainder = bytes.size % 4
|
|
442
|
-
if remainder.positive?
|
|
443
|
-
ul = 0
|
|
444
|
-
offset = bytes.size - remainder
|
|
445
|
-
|
|
446
|
-
case remainder
|
|
447
|
-
when 3
|
|
448
|
-
ul |= bytes[offset + 2] << 16
|
|
449
|
-
ul |= bytes[offset + 1] << 8
|
|
450
|
-
ul |= bytes[offset]
|
|
451
|
-
when 2
|
|
452
|
-
ul |= bytes[offset + 1] << 8
|
|
453
|
-
ul |= bytes[offset]
|
|
454
|
-
when 1
|
|
455
|
-
ul |= bytes[offset]
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
cksum ^= ul
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
cksum & 0xFFFFFFFF
|
|
428
|
+
Checksum.calculate(data, initial)
|
|
462
429
|
end
|
|
463
430
|
end
|
|
464
431
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fractor"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
module CAB
|
|
7
|
+
# Work item for compressing a single file in a CAB archive
|
|
8
|
+
class FileCompressionWork < Fractor::Work
|
|
9
|
+
# Initialize work item for file compression
|
|
10
|
+
#
|
|
11
|
+
# @param source_path [String] Path to source file
|
|
12
|
+
# @param compression_method [Symbol] Compression method to use
|
|
13
|
+
# @param block_size [Integer] Maximum block size
|
|
14
|
+
# @param io_system [System::IOSystem] I/O system
|
|
15
|
+
# @param algorithm_factory [AlgorithmFactory] Algorithm factory
|
|
16
|
+
def initialize(source_path:, compression_method:, block_size:,
|
|
17
|
+
io_system:, algorithm_factory:)
|
|
18
|
+
super({
|
|
19
|
+
source_path: source_path,
|
|
20
|
+
compression_method: compression_method,
|
|
21
|
+
block_size: block_size,
|
|
22
|
+
io_system: io_system,
|
|
23
|
+
algorithm_factory: algorithm_factory,
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def source_path
|
|
28
|
+
input[:source_path]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def compression_method
|
|
32
|
+
input[:compression_method]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def block_size
|
|
36
|
+
input[:block_size]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def io_system
|
|
40
|
+
input[:io_system]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def algorithm_factory
|
|
44
|
+
input[:algorithm_factory]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def id
|
|
48
|
+
source_path
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module CAB
|
|
5
|
+
# Worker for compressing files in a CAB archive
|
|
6
|
+
class FileCompressionWorker < Fractor::Worker
|
|
7
|
+
# Process a file compression work item
|
|
8
|
+
#
|
|
9
|
+
# @param work [FileCompressionWork] Work item to process
|
|
10
|
+
# @return [Fractor::WorkResult] Result with compressed blocks
|
|
11
|
+
def process(work)
|
|
12
|
+
# Read source file
|
|
13
|
+
file_data = ::File.binread(work.source_path)
|
|
14
|
+
file_size = file_data.bytesize
|
|
15
|
+
|
|
16
|
+
# Split into blocks and compress
|
|
17
|
+
blocks = []
|
|
18
|
+
offset = 0
|
|
19
|
+
|
|
20
|
+
while offset < file_size
|
|
21
|
+
remaining = file_size - offset
|
|
22
|
+
chunk_size = [work.block_size, remaining].min
|
|
23
|
+
chunk = file_data[offset, chunk_size]
|
|
24
|
+
|
|
25
|
+
# Compress chunk
|
|
26
|
+
compressed_chunk = compress_chunk(chunk, work)
|
|
27
|
+
|
|
28
|
+
blocks << {
|
|
29
|
+
uncompressed_size: chunk.bytesize,
|
|
30
|
+
compressed_size: compressed_chunk.bytesize,
|
|
31
|
+
data: compressed_chunk,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
offset += chunk_size
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return success result
|
|
38
|
+
Fractor::WorkResult.new(
|
|
39
|
+
result: {
|
|
40
|
+
source_path: work.source_path,
|
|
41
|
+
blocks: blocks,
|
|
42
|
+
total_uncompressed: file_size,
|
|
43
|
+
total_compressed: blocks.sum { |b| b[:compressed_size] },
|
|
44
|
+
},
|
|
45
|
+
work: work,
|
|
46
|
+
)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
# Return error result
|
|
49
|
+
Fractor::WorkResult.new(
|
|
50
|
+
error: {
|
|
51
|
+
message: e.message,
|
|
52
|
+
class: e.class.name,
|
|
53
|
+
source_path: work.source_path,
|
|
54
|
+
},
|
|
55
|
+
work: work,
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Compress a single chunk of data
|
|
62
|
+
#
|
|
63
|
+
# @param chunk [String] Data chunk to compress
|
|
64
|
+
# @param work [FileCompressionWork] Work item with compression settings
|
|
65
|
+
# @return [String] Compressed data
|
|
66
|
+
def compress_chunk(chunk, work)
|
|
67
|
+
input_handle = System::MemoryHandle.new(chunk)
|
|
68
|
+
output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
compressor = work.algorithm_factory.create(
|
|
72
|
+
work.compression_method,
|
|
73
|
+
:compressor,
|
|
74
|
+
work.io_system,
|
|
75
|
+
input_handle,
|
|
76
|
+
output_handle,
|
|
77
|
+
chunk.bytesize,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
compressor.compress
|
|
81
|
+
|
|
82
|
+
output_handle.data
|
|
83
|
+
|
|
84
|
+
# Memory handles don't need closing
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
# Utility module for checksum calculations
|
|
5
|
+
module Checksum
|
|
6
|
+
# Calculate CAB-style checksum (XOR-based)
|
|
7
|
+
#
|
|
8
|
+
# @param data [String] Data to calculate checksum for
|
|
9
|
+
# @param initial [Integer] Initial checksum value (default: 0)
|
|
10
|
+
# @return [Integer] Checksum value (32-bit)
|
|
11
|
+
def self.calculate(data, initial = 0)
|
|
12
|
+
cksum = initial
|
|
13
|
+
bytes = data.bytes
|
|
14
|
+
|
|
15
|
+
# Process 4-byte chunks
|
|
16
|
+
(bytes.size / 4).times do |i|
|
|
17
|
+
offset = i * 4
|
|
18
|
+
value = bytes[offset] |
|
|
19
|
+
(bytes[offset + 1] << 8) |
|
|
20
|
+
(bytes[offset + 2] << 16) |
|
|
21
|
+
(bytes[offset + 3] << 24)
|
|
22
|
+
cksum ^= value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Process remaining bytes
|
|
26
|
+
remainder = bytes.size % 4
|
|
27
|
+
if remainder.positive?
|
|
28
|
+
ul = 0
|
|
29
|
+
offset = bytes.size - remainder
|
|
30
|
+
|
|
31
|
+
case remainder
|
|
32
|
+
when 3
|
|
33
|
+
ul |= bytes[offset + 2] << 16
|
|
34
|
+
ul |= bytes[offset + 1] << 8
|
|
35
|
+
ul |= bytes[offset]
|
|
36
|
+
when 2
|
|
37
|
+
ul |= bytes[offset + 1] << 8
|
|
38
|
+
ul |= bytes[offset]
|
|
39
|
+
when 1
|
|
40
|
+
ul |= bytes[offset]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
cksum ^= ul
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
cksum & 0xFFFFFFFF
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|