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
|
@@ -13,8 +13,9 @@ module Cabriolet
|
|
|
13
13
|
|
|
14
14
|
attr_reader :io_system, :chm
|
|
15
15
|
|
|
16
|
-
def initialize(io_system = nil)
|
|
16
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
17
17
|
@io_system = io_system || System::IOSystem.new
|
|
18
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
18
19
|
@chm = nil
|
|
19
20
|
@input_handle = nil
|
|
20
21
|
@lzx_state = nil
|
|
@@ -72,7 +73,7 @@ module Cabriolet
|
|
|
72
73
|
when 1
|
|
73
74
|
extract_compressed(file, output_path)
|
|
74
75
|
else
|
|
75
|
-
raise
|
|
76
|
+
raise Cabriolet::FormatError, "Invalid section ID: #{file.section.id}"
|
|
76
77
|
end
|
|
77
78
|
end
|
|
78
79
|
|
|
@@ -108,13 +109,20 @@ module Cabriolet
|
|
|
108
109
|
while remaining.positive?
|
|
109
110
|
chunk_size = [buffer_size, remaining].min
|
|
110
111
|
data = @input_handle.read(chunk_size)
|
|
111
|
-
if data.nil?
|
|
112
|
-
raise
|
|
112
|
+
if data.nil?
|
|
113
|
+
raise Cabriolet::ReadError,
|
|
114
|
+
"Unexpected end of file"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# It's OK if we read less than chunk_size (e.g., last chunk or EOF)
|
|
118
|
+
# Only raise an error if we read nothing when we expected data
|
|
119
|
+
if data.empty? && remaining.positive?
|
|
120
|
+
raise Cabriolet::ReadError,
|
|
113
121
|
"Unexpected end of file"
|
|
114
122
|
end
|
|
115
123
|
|
|
116
124
|
output_handle.write(data)
|
|
117
|
-
remaining -=
|
|
125
|
+
remaining -= data.length
|
|
118
126
|
end
|
|
119
127
|
|
|
120
128
|
output_handle.close
|
|
@@ -174,15 +182,18 @@ module Cabriolet
|
|
|
174
182
|
control = sec.control || find_system_file(Parser::CONTROL_NAME)
|
|
175
183
|
|
|
176
184
|
unless content
|
|
177
|
-
raise
|
|
185
|
+
raise Cabriolet::FormatError,
|
|
178
186
|
"MSCompressed Content file not found"
|
|
179
187
|
end
|
|
180
|
-
|
|
188
|
+
unless control
|
|
189
|
+
raise Cabriolet::FormatError,
|
|
190
|
+
"ControlData file not found"
|
|
191
|
+
end
|
|
181
192
|
|
|
182
193
|
# Read control data
|
|
183
194
|
control_data = read_system_file(control)
|
|
184
195
|
unless control_data.length == 28
|
|
185
|
-
raise
|
|
196
|
+
raise Cabriolet::FormatError,
|
|
186
197
|
"ControlData wrong size"
|
|
187
198
|
end
|
|
188
199
|
|
|
@@ -198,13 +209,14 @@ module Cabriolet
|
|
|
198
209
|
when 0x100000 then 20
|
|
199
210
|
when 0x200000 then 21
|
|
200
211
|
else
|
|
201
|
-
raise
|
|
212
|
+
raise Cabriolet::FormatError,
|
|
202
213
|
"Invalid window size: #{window_size}"
|
|
203
214
|
end
|
|
204
215
|
|
|
205
216
|
# Validate reset interval
|
|
206
217
|
if reset_interval.zero? || (reset_interval % LZX_FRAME_SIZE) != 0
|
|
207
|
-
raise
|
|
218
|
+
raise Cabriolet::FormatError,
|
|
219
|
+
"Invalid reset interval: #{reset_interval}"
|
|
208
220
|
end
|
|
209
221
|
|
|
210
222
|
# Find reset table entry for this file
|
|
@@ -227,7 +239,9 @@ module Cabriolet
|
|
|
227
239
|
output_handle = System::MemoryHandle.new("")
|
|
228
240
|
|
|
229
241
|
# Initialize LZX decompressor
|
|
230
|
-
@lzx_state =
|
|
242
|
+
@lzx_state = @algorithm_factory.create(
|
|
243
|
+
Constants::COMP_TYPE_LZX,
|
|
244
|
+
:decompressor,
|
|
231
245
|
@io_system,
|
|
232
246
|
@input_handle,
|
|
233
247
|
output_handle,
|
|
@@ -242,7 +256,7 @@ module Cabriolet
|
|
|
242
256
|
def parse_control_data(data)
|
|
243
257
|
signature = data[4, 4]
|
|
244
258
|
unless signature == "LZXC"
|
|
245
|
-
raise
|
|
259
|
+
raise Cabriolet::SignatureError,
|
|
246
260
|
"Invalid LZXC signature"
|
|
247
261
|
end
|
|
248
262
|
|
|
@@ -255,7 +269,8 @@ module Cabriolet
|
|
|
255
269
|
reset_interval *= LZX_FRAME_SIZE
|
|
256
270
|
window_size *= LZX_FRAME_SIZE
|
|
257
271
|
elsif version != 1
|
|
258
|
-
raise
|
|
272
|
+
raise Cabriolet::FormatError,
|
|
273
|
+
"Unknown ControlData version: #{version}"
|
|
259
274
|
end
|
|
260
275
|
|
|
261
276
|
[window_size, reset_interval]
|
|
@@ -272,7 +287,7 @@ module Cabriolet
|
|
|
272
287
|
# Fall back to SpanInfo
|
|
273
288
|
spaninfo = sec.spaninfo || find_system_file(Parser::SPANINFO_NAME)
|
|
274
289
|
unless spaninfo
|
|
275
|
-
raise
|
|
290
|
+
raise Cabriolet::FormatError,
|
|
276
291
|
"Neither ResetTable nor SpanInfo found"
|
|
277
292
|
end
|
|
278
293
|
|
|
@@ -284,12 +299,12 @@ module Cabriolet
|
|
|
284
299
|
# Read an entry from the reset table
|
|
285
300
|
def read_reset_table_entry(rtable, entry, reset_interval)
|
|
286
301
|
data = read_system_file(rtable)
|
|
287
|
-
raise
|
|
302
|
+
raise Cabriolet::FormatError, "ResetTable too short" if data.length < 40
|
|
288
303
|
|
|
289
304
|
# Check frame length
|
|
290
305
|
frame_len = data[32, 8].unpack1("Q<")
|
|
291
306
|
unless frame_len == LZX_FRAME_SIZE
|
|
292
|
-
raise
|
|
307
|
+
raise Cabriolet::FormatError,
|
|
293
308
|
"Invalid frame length"
|
|
294
309
|
end
|
|
295
310
|
|
|
@@ -307,7 +322,7 @@ module Cabriolet
|
|
|
307
322
|
when 4 then data[pos, 4].unpack1("V")
|
|
308
323
|
when 8 then data[pos, 8].unpack1("Q<")
|
|
309
324
|
else
|
|
310
|
-
raise
|
|
325
|
+
raise Cabriolet::FormatError,
|
|
311
326
|
"Invalid entry size: #{entry_size}"
|
|
312
327
|
end
|
|
313
328
|
|
|
@@ -325,11 +340,14 @@ module Cabriolet
|
|
|
325
340
|
# Read SpanInfo to get uncompressed length
|
|
326
341
|
def read_spaninfo(spaninfo)
|
|
327
342
|
data = read_system_file(spaninfo)
|
|
328
|
-
|
|
343
|
+
unless data.length == 8
|
|
344
|
+
raise Cabriolet::FormatError,
|
|
345
|
+
"SpanInfo wrong size"
|
|
346
|
+
end
|
|
329
347
|
|
|
330
348
|
length = data.unpack1("Q<")
|
|
331
349
|
unless length.positive?
|
|
332
|
-
raise
|
|
350
|
+
raise Cabriolet::FormatError,
|
|
333
351
|
"Invalid SpanInfo length"
|
|
334
352
|
end
|
|
335
353
|
|
|
@@ -350,7 +368,7 @@ module Cabriolet
|
|
|
350
368
|
# Read a system file's contents
|
|
351
369
|
def read_system_file(file)
|
|
352
370
|
unless file.section.id.zero?
|
|
353
|
-
raise
|
|
371
|
+
raise Cabriolet::FormatError,
|
|
354
372
|
"System file must be in section 0"
|
|
355
373
|
end
|
|
356
374
|
|
|
@@ -417,7 +435,7 @@ module Cabriolet
|
|
|
417
435
|
file.length = length
|
|
418
436
|
return file
|
|
419
437
|
end
|
|
420
|
-
rescue
|
|
438
|
+
rescue Cabriolet::FormatError
|
|
421
439
|
break
|
|
422
440
|
end
|
|
423
441
|
end
|
data/lib/cabriolet/chm/parser.rb
CHANGED
|
@@ -56,9 +56,12 @@ module Cabriolet
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Check GUIDs
|
|
59
|
-
|
|
59
|
+
# Note: Some CHM files have both GUIDs set to GUID1 (unusual but valid)
|
|
60
|
+
# Standard files have GUID1 and GUID2 as expected
|
|
61
|
+
# We validate that guid2 matches either GUID1 or GUID2
|
|
62
|
+
unless [GUID1, GUID2].include?(header.guid2)
|
|
60
63
|
raise SignatureError,
|
|
61
|
-
"Invalid CHM GUIDs"
|
|
64
|
+
"Invalid CHM GUIDs (guid2 should match CHM format GUID)"
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
@chm.version = header.version
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Commands
|
|
5
|
+
# Abstract base class for format-specific command handlers
|
|
6
|
+
#
|
|
7
|
+
# This class defines the interface that all format command handlers must implement.
|
|
8
|
+
# Each format (CAB, CHM, SZDD, KWAJ, HLP, LIT, OAB) should have its own
|
|
9
|
+
# CommandHandler subclass that inherits from this base class.
|
|
10
|
+
#
|
|
11
|
+
# The base class provides common functionality and enforces a consistent
|
|
12
|
+
# interface across all format handlers, following the Template Method pattern.
|
|
13
|
+
#
|
|
14
|
+
# @example Creating a format handler
|
|
15
|
+
# module Cabriolet
|
|
16
|
+
# module CAB
|
|
17
|
+
# class CommandHandler < CLI::BaseCommandHandler
|
|
18
|
+
# def list(file, options = {})
|
|
19
|
+
# # Implementation for listing CAB files
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class BaseCommandHandler
|
|
26
|
+
# Initialize the command handler
|
|
27
|
+
#
|
|
28
|
+
# @param verbose [Boolean] Enable verbose output
|
|
29
|
+
def initialize(verbose: false)
|
|
30
|
+
@verbose = verbose
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# List archive contents
|
|
34
|
+
#
|
|
35
|
+
# @param file [String] Path to the archive file
|
|
36
|
+
# @param options [Hash] Additional options
|
|
37
|
+
# @raise [NotImplementedError] Subclass must implement
|
|
38
|
+
def list(file, options = {})
|
|
39
|
+
raise NotImplementedError,
|
|
40
|
+
"#{self.class} must implement #list"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Extract files from archive
|
|
44
|
+
#
|
|
45
|
+
# @param file [String] Path to the archive file
|
|
46
|
+
# @param output_dir [String] Output directory path
|
|
47
|
+
# @param options [Hash] Additional options
|
|
48
|
+
# @raise [NotImplementedError] Subclass must implement
|
|
49
|
+
def extract(file, output_dir, options = {})
|
|
50
|
+
raise NotImplementedError,
|
|
51
|
+
"#{self.class} must implement #extract"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Create a new archive
|
|
55
|
+
#
|
|
56
|
+
# @param output [String] Output file path
|
|
57
|
+
# @param files [Array<String>] List of input files
|
|
58
|
+
# @param options [Hash] Additional options
|
|
59
|
+
# @raise [NotImplementedError] Subclass must implement
|
|
60
|
+
def create(output, files, options = {})
|
|
61
|
+
raise NotImplementedError,
|
|
62
|
+
"#{self.class} must implement #create"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Display archive information
|
|
66
|
+
#
|
|
67
|
+
# @param file [String] Path to the archive file
|
|
68
|
+
# @param options [Hash] Additional options
|
|
69
|
+
# @raise [NotImplementedError] Subclass must implement
|
|
70
|
+
def info(file, options = {})
|
|
71
|
+
raise NotImplementedError,
|
|
72
|
+
"#{self.class} must implement #info"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Test archive integrity
|
|
76
|
+
#
|
|
77
|
+
# @param file [String] Path to the archive file
|
|
78
|
+
# @param options [Hash] Additional options
|
|
79
|
+
# @raise [NotImplementedError] Subclass must implement
|
|
80
|
+
def test(file, options = {})
|
|
81
|
+
raise NotImplementedError,
|
|
82
|
+
"#{self.class} must implement #test"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
protected
|
|
86
|
+
|
|
87
|
+
# Check if verbose output is enabled
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] true if verbose mode is active
|
|
90
|
+
def verbose?
|
|
91
|
+
@verbose
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Detect format from file using FormatDetector
|
|
95
|
+
#
|
|
96
|
+
# This is a convenience method for handlers that need to perform
|
|
97
|
+
# format detection within their operations.
|
|
98
|
+
#
|
|
99
|
+
# @param file [String] Path to the file
|
|
100
|
+
# @return [Symbol, nil] Detected format symbol
|
|
101
|
+
def detect_format(file)
|
|
102
|
+
require_relative "../format_detector"
|
|
103
|
+
FormatDetector.detect(file)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Validate that a file exists
|
|
107
|
+
#
|
|
108
|
+
# @param file [String] Path to the file
|
|
109
|
+
# @raise [ArgumentError] if file doesn't exist
|
|
110
|
+
def validate_file_exists(file)
|
|
111
|
+
return if File.exist?(file)
|
|
112
|
+
|
|
113
|
+
raise ArgumentError, "File does not exist: #{file}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Ensure output directory exists
|
|
117
|
+
#
|
|
118
|
+
# @param output_dir [String] Output directory path
|
|
119
|
+
# @return [String] The output directory path
|
|
120
|
+
def ensure_output_dir(output_dir)
|
|
121
|
+
require "fileutils"
|
|
122
|
+
FileUtils.mkdir_p(output_dir)
|
|
123
|
+
output_dir
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_registry"
|
|
4
|
+
require_relative "base_command_handler"
|
|
5
|
+
require_relative "../format_detector"
|
|
6
|
+
|
|
7
|
+
module Cabriolet
|
|
8
|
+
module Commands
|
|
9
|
+
# Unified command dispatcher that routes commands to format-specific handlers
|
|
10
|
+
#
|
|
11
|
+
# The dispatcher is responsible for:
|
|
12
|
+
# 1. Detecting the format of input files (or using manual override)
|
|
13
|
+
# 2. Selecting the appropriate format handler from the registry
|
|
14
|
+
# 3. Delegating command execution to the handler
|
|
15
|
+
#
|
|
16
|
+
# This class implements the Strategy pattern, where the format handler
|
|
17
|
+
# is the strategy selected based on the detected format.
|
|
18
|
+
#
|
|
19
|
+
# @example Using the dispatcher
|
|
20
|
+
# dispatcher = CLI::CommandDispatcher.new(format: :cab, verbose: true)
|
|
21
|
+
# dispatcher.dispatch(:list, "archive.cab")
|
|
22
|
+
#
|
|
23
|
+
class CommandDispatcher
|
|
24
|
+
# Initialize the command dispatcher
|
|
25
|
+
#
|
|
26
|
+
# @param options [Hash] Configuration options
|
|
27
|
+
# @option options [String, Symbol] :format Manual format override
|
|
28
|
+
# @option options [Boolean] :verbose Enable verbose output
|
|
29
|
+
def initialize(options = {})
|
|
30
|
+
@format_override = parse_format_option(options[:format])
|
|
31
|
+
@verbose = options[:verbose] || false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Dispatch a command to the appropriate format handler
|
|
35
|
+
#
|
|
36
|
+
# This method detects the format (if not manually specified),
|
|
37
|
+
# retrieves the appropriate handler, and delegates the command execution.
|
|
38
|
+
#
|
|
39
|
+
# @param command [Symbol] The command to execute (:list, :extract, etc.)
|
|
40
|
+
# @param file [String] Path to the archive file
|
|
41
|
+
# @param args [Array] Additional positional arguments for the command
|
|
42
|
+
# @param options [Hash] Additional options to pass to the handler
|
|
43
|
+
# @raise [Cabriolet::Error] if format detection fails or handler not found
|
|
44
|
+
# @return [void]
|
|
45
|
+
def dispatch(command, file, *args, **options)
|
|
46
|
+
format = detect_format(file)
|
|
47
|
+
handler = get_handler_for(format)
|
|
48
|
+
|
|
49
|
+
execute_command(handler, command, file, args, options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if a format is supported
|
|
53
|
+
#
|
|
54
|
+
# @param format [Symbol] The format to check
|
|
55
|
+
# @return [Boolean] true if the format has a registered handler
|
|
56
|
+
def self.format_supported?(format)
|
|
57
|
+
CommandRegistry.format_registered?(format)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get list of supported formats
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Symbol>] List of supported format symbols
|
|
63
|
+
def self.supported_formats
|
|
64
|
+
CommandRegistry.registered_formats
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Parse format option to symbol
|
|
70
|
+
#
|
|
71
|
+
# @param format_value [String, Symbol, nil] The format option value
|
|
72
|
+
# @return [Symbol, nil] The format as a symbol
|
|
73
|
+
def parse_format_option(format_value)
|
|
74
|
+
return nil if format_value.nil?
|
|
75
|
+
|
|
76
|
+
format_value.to_sym
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Detect format from file with fallback to manual override
|
|
80
|
+
#
|
|
81
|
+
# @param file [String] Path to the archive file
|
|
82
|
+
# @return [Symbol] The detected format symbol
|
|
83
|
+
# @raise [Cabriolet::Error] if format cannot be detected
|
|
84
|
+
def detect_format(file)
|
|
85
|
+
return @format_override if @format_override
|
|
86
|
+
|
|
87
|
+
format = FormatDetector.detect(file)
|
|
88
|
+
if format.nil?
|
|
89
|
+
supported = CommandRegistry.registered_formats.join(", ")
|
|
90
|
+
raise Error,
|
|
91
|
+
"Cannot detect format for: #{file}. " \
|
|
92
|
+
"Use --format to specify (supported: #{supported})"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
format
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get the handler class for a format
|
|
99
|
+
#
|
|
100
|
+
# @param format [Symbol] The format symbol
|
|
101
|
+
# @return [Class] The handler class
|
|
102
|
+
# @raise [Cabriolet::Error] if no handler is registered
|
|
103
|
+
def get_handler_for(format)
|
|
104
|
+
handler = CommandRegistry.handler_for(format)
|
|
105
|
+
|
|
106
|
+
unless handler
|
|
107
|
+
raise Error,
|
|
108
|
+
"No command handler registered for format: #{format}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
handler
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Execute the command on the handler
|
|
115
|
+
#
|
|
116
|
+
# @param handler_class [Class] The handler class
|
|
117
|
+
# @param command [Symbol] The command to execute
|
|
118
|
+
# @param file [String] Path to the archive file
|
|
119
|
+
# @param args [Array] Additional positional arguments
|
|
120
|
+
# @param options [Hash] Additional options
|
|
121
|
+
# @return [void]
|
|
122
|
+
def execute_command(handler_class, command, file, args, options)
|
|
123
|
+
handler = handler_class.new(verbose: @verbose)
|
|
124
|
+
|
|
125
|
+
# Call the command method with appropriate arguments
|
|
126
|
+
case command
|
|
127
|
+
when :extract
|
|
128
|
+
output_dir = args.first || options[:output_dir]
|
|
129
|
+
handler.extract(file, output_dir, options)
|
|
130
|
+
when :create
|
|
131
|
+
files = args.first || options[:files] || []
|
|
132
|
+
handler.create(file, files, options)
|
|
133
|
+
else
|
|
134
|
+
# For commands that take just file and options
|
|
135
|
+
handler.public_send(command, file, options)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Commands
|
|
5
|
+
# Registry for mapping format symbols to command handler classes
|
|
6
|
+
#
|
|
7
|
+
# This registry provides a centralized location for managing format handlers,
|
|
8
|
+
# following the Open/Closed Principle - new formats can be added without
|
|
9
|
+
# modifying existing command logic.
|
|
10
|
+
#
|
|
11
|
+
# @example Registering a format handler
|
|
12
|
+
# CLI::CommandRegistry.register_format(:cab, CAB::CommandHandler)
|
|
13
|
+
#
|
|
14
|
+
# @example Getting a handler for a format
|
|
15
|
+
# handler = CLI::CommandRegistry.handler_for(:cab)
|
|
16
|
+
#
|
|
17
|
+
class CommandRegistry
|
|
18
|
+
@handlers = {}
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Get the command handler class for a given format
|
|
22
|
+
#
|
|
23
|
+
# @param format [Symbol] The format symbol (e.g., :cab, :chm, :szdd)
|
|
24
|
+
# @return [Class, nil] The handler class or nil if not registered
|
|
25
|
+
def handler_for(format)
|
|
26
|
+
@handlers[format]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Register a command handler for a format
|
|
30
|
+
#
|
|
31
|
+
# This allows for dynamic registration of format handlers,
|
|
32
|
+
# supporting extensibility and plugin architectures.
|
|
33
|
+
#
|
|
34
|
+
# @param format [Symbol] The format symbol
|
|
35
|
+
# @param handler_class [Class] The command handler class
|
|
36
|
+
# @raise [ArgumentError] if handler_class doesn't implement required interface
|
|
37
|
+
def register_format(format, handler_class)
|
|
38
|
+
validate_handler_interface(handler_class)
|
|
39
|
+
@handlers[format] = handler_class
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get all registered formats
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<Symbol>] List of registered format symbols
|
|
45
|
+
def registered_formats
|
|
46
|
+
@handlers.keys
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if a format is registered
|
|
50
|
+
#
|
|
51
|
+
# @param format [Symbol] The format symbol
|
|
52
|
+
# @return [Boolean] true if the format has a registered handler
|
|
53
|
+
def format_registered?(format)
|
|
54
|
+
@handlers.key?(format)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Clear all registered formats (primarily for testing)
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
def clear
|
|
61
|
+
@handlers = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Validate that a handler class implements the required interface
|
|
67
|
+
#
|
|
68
|
+
# @param handler_class [Class] The class to validate
|
|
69
|
+
# @raise [ArgumentError] if the class doesn't implement required methods
|
|
70
|
+
def validate_handler_interface(handler_class)
|
|
71
|
+
required_methods = %i[list extract create info test]
|
|
72
|
+
|
|
73
|
+
required_methods.each do |method|
|
|
74
|
+
unless handler_class.method_defined?(method)
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"Handler class #{handler_class} must implement ##{method}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|