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.
- checksums.yaml +4 -4
- data/README.adoc +700 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +154 -14
- data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +35 -43
- data/lib/cabriolet/cab/decompressor.rb +14 -19
- data/lib/cabriolet/cab/extractor.rb +140 -31
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +34 -45
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +3 -2
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +633 -38
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +67 -17
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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,
|
|
166
|
-
if block_data.length <
|
|
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 =
|
|
249
|
-
|
|
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 =
|
|
290
|
-
|
|
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
|