cabriolet 0.1.2 → 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 +703 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +167 -16
- data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
- 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 +108 -84
- data/lib/cabriolet/cab/decompressor.rb +16 -20
- data/lib/cabriolet/cab/extractor.rb +142 -66
- 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/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/collections/file_collection.rb +175 -0
- 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 +36 -95
- 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 +83 -53
- data/lib/cabriolet/errors.rb +3 -0
- 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/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/format_base.rb +79 -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 +151 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -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/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -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/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/encoder.rb +15 -12
- 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 +119 -168
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- 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/parser.rb +670 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -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/quantum_shared.rb +105 -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 +181 -20
- metadata +69 -4
- data/lib/cabriolet/auto.rb +0 -173
- data/lib/cabriolet/parallel.rb +0 -333
|
@@ -0,0 +1,217 @@
|
|
|
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 SZDD
|
|
9
|
+
# Command handler for SZDD (LZSS-compressed) format
|
|
10
|
+
#
|
|
11
|
+
# This handler implements the unified command interface for SZDD files,
|
|
12
|
+
# wrapping the existing SZDD::Decompressor and SZDD::Compressor classes.
|
|
13
|
+
#
|
|
14
|
+
class CommandHandler < Commands::BaseCommandHandler
|
|
15
|
+
# List SZDD file information
|
|
16
|
+
#
|
|
17
|
+
# For SZDD files, list displays detailed file information
|
|
18
|
+
# rather than a file listing (single file archive).
|
|
19
|
+
#
|
|
20
|
+
# @param file [String] Path to the SZDD file
|
|
21
|
+
# @param options [Hash] Additional options (unused)
|
|
22
|
+
# @return [void]
|
|
23
|
+
def list(file, _options = {})
|
|
24
|
+
validate_file_exists(file)
|
|
25
|
+
|
|
26
|
+
decompressor = Decompressor.new
|
|
27
|
+
header = decompressor.open(file)
|
|
28
|
+
|
|
29
|
+
display_szdd_info(header, file)
|
|
30
|
+
|
|
31
|
+
decompressor.close(header)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Extract SZDD compressed file
|
|
35
|
+
#
|
|
36
|
+
# Expands the SZDD file to its original form.
|
|
37
|
+
# Auto-detects output filename if not specified.
|
|
38
|
+
#
|
|
39
|
+
# @param file [String] Path to the SZDD file
|
|
40
|
+
# @param output [String, nil] Output file path (or directory, for single-file extraction)
|
|
41
|
+
# @param options [Hash] Additional options
|
|
42
|
+
# @option options [String] :output Output file path
|
|
43
|
+
# @return [void]
|
|
44
|
+
def extract(file, output = nil, options = {})
|
|
45
|
+
validate_file_exists(file)
|
|
46
|
+
|
|
47
|
+
# Use output file from options if specified, otherwise use positional argument
|
|
48
|
+
output ||= options[:output]
|
|
49
|
+
|
|
50
|
+
# Auto-detect output name if not provided
|
|
51
|
+
output ||= auto_output_filename(file)
|
|
52
|
+
|
|
53
|
+
decompressor = Decompressor.new
|
|
54
|
+
header = decompressor.open(file)
|
|
55
|
+
|
|
56
|
+
puts "Expanding #{file} -> #{output}" if verbose?
|
|
57
|
+
bytes = decompressor.extract(header, output)
|
|
58
|
+
decompressor.close(header)
|
|
59
|
+
|
|
60
|
+
puts "Expanded #{file} to #{output} (#{bytes} bytes)"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Create SZDD compressed file
|
|
64
|
+
#
|
|
65
|
+
# Compresses a file using SZDD (LZSS) compression.
|
|
66
|
+
#
|
|
67
|
+
# @param output [String] Output SZDD file path
|
|
68
|
+
# @param files [Array<String>] Input file (single file for SZDD)
|
|
69
|
+
# @param options [Hash] Additional options
|
|
70
|
+
# @option options [String] :missing_char Missing character for filename
|
|
71
|
+
# @option options [String] :szdd_format SZDD format (:normal, :qbasic)
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @raise [ArgumentError] if no file specified or multiple files
|
|
74
|
+
def create(output, files = [], options = {})
|
|
75
|
+
raise ArgumentError, "No file specified" if files.empty?
|
|
76
|
+
|
|
77
|
+
if files.size > 1
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"SZDD format supports only one file at a time"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
file = files.first
|
|
83
|
+
unless File.exist?(file)
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
"File does not exist: #{file}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
format = parse_format_option(options[:szdd_format])
|
|
89
|
+
compress_options = { format: format }
|
|
90
|
+
if options[:missing_char]
|
|
91
|
+
compress_options[:missing_char] =
|
|
92
|
+
options[:missing_char]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Auto-generate output name if not provided
|
|
96
|
+
if output.nil?
|
|
97
|
+
output = auto_generate_output(file)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
compressor = Compressor.new
|
|
101
|
+
|
|
102
|
+
puts "Compressing #{file} -> #{output}" if verbose?
|
|
103
|
+
bytes = compressor.compress(file, output, **compress_options)
|
|
104
|
+
|
|
105
|
+
puts "Compressed #{file} to #{output} (#{bytes} bytes)"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Display detailed SZDD file information
|
|
109
|
+
#
|
|
110
|
+
# @param file [String] Path to the SZDD file
|
|
111
|
+
# @param options [Hash] Additional options (unused)
|
|
112
|
+
# @return [void]
|
|
113
|
+
def info(file, _options = {})
|
|
114
|
+
validate_file_exists(file)
|
|
115
|
+
|
|
116
|
+
decompressor = Decompressor.new
|
|
117
|
+
header = decompressor.open(file)
|
|
118
|
+
|
|
119
|
+
display_szdd_info(header, file)
|
|
120
|
+
|
|
121
|
+
decompressor.close(header)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Test SZDD file integrity
|
|
125
|
+
#
|
|
126
|
+
# Verifies the SZDD file structure.
|
|
127
|
+
#
|
|
128
|
+
# @param file [String] Path to the SZDD file
|
|
129
|
+
# @param options [Hash] Additional options (unused)
|
|
130
|
+
# @return [void]
|
|
131
|
+
def test(file, _options = {})
|
|
132
|
+
validate_file_exists(file)
|
|
133
|
+
|
|
134
|
+
decompressor = Decompressor.new
|
|
135
|
+
header = decompressor.open(file)
|
|
136
|
+
|
|
137
|
+
puts "Testing #{file}..."
|
|
138
|
+
# TODO: Implement full integrity testing
|
|
139
|
+
puts "OK: SZDD file structure is valid"
|
|
140
|
+
puts "Format: #{header.format.to_s.upcase}"
|
|
141
|
+
puts "Uncompressed size: #{header.length} bytes"
|
|
142
|
+
|
|
143
|
+
decompressor.close(header)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Display SZDD file information
|
|
149
|
+
#
|
|
150
|
+
# @param header [Header] The SZDD header object
|
|
151
|
+
# @param file [String] Original file path
|
|
152
|
+
# @return [void]
|
|
153
|
+
def display_szdd_info(header, file)
|
|
154
|
+
puts "SZDD File Information"
|
|
155
|
+
puts "=" * 50
|
|
156
|
+
puts "Filename: #{file}"
|
|
157
|
+
puts "Format: #{header.format.to_s.upcase}"
|
|
158
|
+
puts "Uncompressed size: #{header.length} bytes"
|
|
159
|
+
if header.missing_char
|
|
160
|
+
puts "Missing character: '#{header.missing_char}'"
|
|
161
|
+
puts "Suggested filename: #{header.suggested_filename(File.basename(file))}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Auto-detect output filename from SZDD header
|
|
166
|
+
#
|
|
167
|
+
# @param file [String] Original file path
|
|
168
|
+
# @return [String] Detected output filename
|
|
169
|
+
def auto_output_filename(file)
|
|
170
|
+
decompressor = Decompressor.new
|
|
171
|
+
header = decompressor.open(file)
|
|
172
|
+
output = decompressor.auto_output_filename(file, header)
|
|
173
|
+
decompressor.close(header)
|
|
174
|
+
output
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Auto-generate output filename for SZDD
|
|
178
|
+
#
|
|
179
|
+
# SZDD convention: file.txt -> file.tx_
|
|
180
|
+
#
|
|
181
|
+
# @param file [String] Original file path
|
|
182
|
+
# @return [String] Generated output filename
|
|
183
|
+
def auto_generate_output(file)
|
|
184
|
+
# Replace extension last character with underscore
|
|
185
|
+
# file.txt -> file.tx_
|
|
186
|
+
ext = File.extname(file)
|
|
187
|
+
if ext.length == 2 # Single char extension like .c
|
|
188
|
+
base = File.basename(file, ext)
|
|
189
|
+
output = "#{base}#{ext[0]}_"
|
|
190
|
+
else
|
|
191
|
+
# For no extension or multi-char extension, just append _
|
|
192
|
+
output = "#{file}_"
|
|
193
|
+
end
|
|
194
|
+
output
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Parse format option to symbol
|
|
198
|
+
#
|
|
199
|
+
# @param format_value [String, Symbol] The format type
|
|
200
|
+
# @return [Symbol] The format symbol
|
|
201
|
+
def parse_format_option(format_value)
|
|
202
|
+
return :normal if format_value.nil?
|
|
203
|
+
|
|
204
|
+
format = format_value.to_sym
|
|
205
|
+
valid_formats = %i[normal qbasic]
|
|
206
|
+
|
|
207
|
+
unless valid_formats.include?(format)
|
|
208
|
+
raise ArgumentError,
|
|
209
|
+
"Invalid SZDD format: #{format_value}. " \
|
|
210
|
+
"Valid options: #{valid_formats.join(', ')}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
format
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -14,8 +14,10 @@ module Cabriolet
|
|
|
14
14
|
#
|
|
15
15
|
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
16
16
|
# default
|
|
17
|
-
|
|
17
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
18
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
18
19
|
@io_system = io_system || System::IOSystem.new
|
|
20
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
# Compress a file to SZDD format
|
|
@@ -59,12 +61,14 @@ module Cabriolet
|
|
|
59
61
|
Compressors::LZSS::MODE_QBASIC
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
compressor =
|
|
64
|
+
compressor = @algorithm_factory.create(
|
|
65
|
+
:lzss,
|
|
66
|
+
:compressor,
|
|
63
67
|
@io_system,
|
|
64
68
|
input_handle,
|
|
65
69
|
output_handle,
|
|
66
70
|
2048,
|
|
67
|
-
lzss_mode,
|
|
71
|
+
mode: lzss_mode,
|
|
68
72
|
)
|
|
69
73
|
|
|
70
74
|
compressed_bytes = compressor.compress
|
|
@@ -107,18 +111,18 @@ module Cabriolet
|
|
|
107
111
|
)
|
|
108
112
|
|
|
109
113
|
# Compress data using LZSS
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
Compressors::LZSS::MODE_QBASIC
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
compressor = Compressors::LZSS.new(
|
|
114
|
+
compressor = @algorithm_factory.create(
|
|
115
|
+
:lzss,
|
|
116
|
+
:compressor,
|
|
117
117
|
@io_system,
|
|
118
118
|
input_handle,
|
|
119
119
|
output_handle,
|
|
120
120
|
2048,
|
|
121
|
-
|
|
121
|
+
mode: if format == :normal
|
|
122
|
+
Compressors::LZSS::MODE_EXPAND
|
|
123
|
+
else
|
|
124
|
+
Compressors::LZSS::MODE_QBASIC
|
|
125
|
+
end,
|
|
122
126
|
)
|
|
123
127
|
|
|
124
128
|
compressed_bytes = compressor.compress
|
|
@@ -17,8 +17,10 @@ module Cabriolet
|
|
|
17
17
|
#
|
|
18
18
|
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
19
19
|
# default
|
|
20
|
-
|
|
20
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
21
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
21
22
|
@io_system = io_system || System::IOSystem.new
|
|
23
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
22
24
|
@parser = Parser.new(@io_system)
|
|
23
25
|
@buffer_size = DEFAULT_BUFFER_SIZE
|
|
24
26
|
end
|
|
@@ -70,16 +72,18 @@ module Cabriolet
|
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
# Create LZSS decompressor
|
|
73
|
-
decompressor =
|
|
75
|
+
decompressor = @algorithm_factory.create(
|
|
76
|
+
:lzss,
|
|
77
|
+
:decompressor,
|
|
74
78
|
@io_system,
|
|
75
79
|
input_handle,
|
|
76
80
|
output_handle,
|
|
77
81
|
@buffer_size,
|
|
78
|
-
lzss_mode,
|
|
82
|
+
mode: lzss_mode,
|
|
79
83
|
)
|
|
80
84
|
|
|
81
|
-
# Decompress
|
|
82
|
-
bytes_written = decompressor.decompress(
|
|
85
|
+
# Decompress (SZDD reads until EOF, no compressed size stored)
|
|
86
|
+
bytes_written = decompressor.decompress(nil)
|
|
83
87
|
|
|
84
88
|
# Verify decompressed size matches expected
|
|
85
89
|
if bytes_written != header.length && Cabriolet.verbose && Cabriolet.verbose
|
|
@@ -118,16 +122,18 @@ module Cabriolet
|
|
|
118
122
|
end
|
|
119
123
|
|
|
120
124
|
# Create LZSS decompressor
|
|
121
|
-
decompressor =
|
|
125
|
+
decompressor = @algorithm_factory.create(
|
|
126
|
+
:lzss,
|
|
127
|
+
:decompressor,
|
|
122
128
|
@io_system,
|
|
123
129
|
input_handle,
|
|
124
130
|
output_handle,
|
|
125
131
|
@buffer_size,
|
|
126
|
-
lzss_mode,
|
|
132
|
+
mode: lzss_mode,
|
|
127
133
|
)
|
|
128
134
|
|
|
129
|
-
# Decompress
|
|
130
|
-
decompressor.decompress(
|
|
135
|
+
# Decompress (SZDD reads until EOF, no compressed size stored)
|
|
136
|
+
decompressor.decompress(nil)
|
|
131
137
|
|
|
132
138
|
# Return the decompressed data
|
|
133
139
|
output_handle.data
|
|
@@ -175,6 +181,9 @@ module Cabriolet
|
|
|
175
181
|
# Use header's suggested filename method
|
|
176
182
|
suggested = header.suggested_filename(base)
|
|
177
183
|
|
|
184
|
+
# Strip null bytes which can occur in malformed SZDD files
|
|
185
|
+
suggested = suggested.delete("\0")
|
|
186
|
+
|
|
178
187
|
# Combine with original directory
|
|
179
188
|
dir = ::File.dirname(input_path)
|
|
180
189
|
::File.join(dir, suggested)
|
data/lib/cabriolet/version.rb
CHANGED
data/lib/cabriolet.rb
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Cabriolet - Pure Ruby implementation of Microsoft compression formats
|
|
3
4
|
require_relative "cabriolet/version"
|
|
4
|
-
require_relative "cabriolet/platform"
|
|
5
5
|
require_relative "cabriolet/constants"
|
|
6
|
+
require_relative "cabriolet/errors"
|
|
7
|
+
require_relative "cabriolet/platform"
|
|
8
|
+
|
|
9
|
+
# System layer
|
|
10
|
+
require_relative "cabriolet/system/io_system"
|
|
11
|
+
require_relative "cabriolet/system/file_handle"
|
|
12
|
+
require_relative "cabriolet/system/memory_handle"
|
|
13
|
+
|
|
14
|
+
# Binary structures
|
|
15
|
+
require_relative "cabriolet/binary/bitstream"
|
|
16
|
+
require_relative "cabriolet/binary/bitstream_writer"
|
|
17
|
+
require_relative "cabriolet/binary/structures"
|
|
18
|
+
require_relative "cabriolet/binary/chm_structures"
|
|
19
|
+
require_relative "cabriolet/binary/szdd_structures"
|
|
20
|
+
require_relative "cabriolet/binary/kwaj_structures"
|
|
21
|
+
require_relative "cabriolet/binary/hlp_structures"
|
|
22
|
+
require_relative "cabriolet/binary/lit_structures"
|
|
23
|
+
require_relative "cabriolet/binary/oab_structures"
|
|
24
|
+
|
|
25
|
+
# Foundation classes (architectural improvements)
|
|
26
|
+
require_relative "cabriolet/file_entry"
|
|
27
|
+
require_relative "cabriolet/file_manager"
|
|
28
|
+
require_relative "cabriolet/base_compressor"
|
|
29
|
+
require_relative "cabriolet/checksum"
|
|
6
30
|
|
|
7
31
|
# Cabriolet is a pure Ruby library for extracting Microsoft Cabinet (.CAB) files,
|
|
8
32
|
# CHM (Compiled HTML Help) files, and related compression formats.
|
|
@@ -13,40 +37,66 @@ module Cabriolet
|
|
|
13
37
|
|
|
14
38
|
# Default buffer size for I/O operations (4KB)
|
|
15
39
|
attr_accessor :default_buffer_size
|
|
40
|
+
|
|
41
|
+
# Get the global algorithm factory instance
|
|
42
|
+
#
|
|
43
|
+
# @return [AlgorithmFactory] The algorithm factory
|
|
44
|
+
def algorithm_factory
|
|
45
|
+
@algorithm_factory ||= AlgorithmFactory.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set the global algorithm factory instance
|
|
49
|
+
#
|
|
50
|
+
# @param factory [AlgorithmFactory] The algorithm factory to use
|
|
51
|
+
# @return [AlgorithmFactory] The factory
|
|
52
|
+
def algorithm_factory=(factory)
|
|
53
|
+
@algorithm_factory = factory
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get the global plugin manager instance
|
|
57
|
+
#
|
|
58
|
+
# @return [PluginManager] The plugin manager
|
|
59
|
+
def plugin_manager
|
|
60
|
+
PluginManager.instance
|
|
61
|
+
end
|
|
16
62
|
end
|
|
17
63
|
|
|
18
64
|
self.verbose = false
|
|
19
|
-
|
|
65
|
+
# Default buffer size of 64KB - better for modern systems
|
|
66
|
+
# Larger buffers reduce I/O syscall overhead significantly
|
|
67
|
+
self.default_buffer_size = 65_536
|
|
20
68
|
end
|
|
21
69
|
|
|
22
|
-
#
|
|
23
|
-
require_relative "cabriolet/system/io_system"
|
|
24
|
-
require_relative "cabriolet/system/file_handle"
|
|
25
|
-
require_relative "cabriolet/system/memory_handle"
|
|
26
|
-
|
|
27
|
-
require_relative "cabriolet/binary/structures"
|
|
28
|
-
require_relative "cabriolet/binary/bitstream"
|
|
29
|
-
require_relative "cabriolet/binary/bitstream_writer"
|
|
30
|
-
require_relative "cabriolet/binary/chm_structures"
|
|
31
|
-
require_relative "cabriolet/binary/szdd_structures"
|
|
32
|
-
require_relative "cabriolet/binary/kwaj_structures"
|
|
33
|
-
require_relative "cabriolet/binary/hlp_structures"
|
|
34
|
-
require_relative "cabriolet/binary/lit_structures"
|
|
35
|
-
require_relative "cabriolet/binary/oab_structures"
|
|
36
|
-
|
|
70
|
+
# Models
|
|
37
71
|
require_relative "cabriolet/models/cabinet"
|
|
38
72
|
require_relative "cabriolet/models/folder"
|
|
73
|
+
require_relative "cabriolet/models/folder_data"
|
|
39
74
|
require_relative "cabriolet/models/file"
|
|
40
75
|
require_relative "cabriolet/models/chm_header"
|
|
41
|
-
require_relative "cabriolet/models/chm_file"
|
|
42
76
|
require_relative "cabriolet/models/chm_section"
|
|
77
|
+
require_relative "cabriolet/models/chm_file"
|
|
43
78
|
require_relative "cabriolet/models/szdd_header"
|
|
44
79
|
require_relative "cabriolet/models/kwaj_header"
|
|
45
80
|
require_relative "cabriolet/models/hlp_header"
|
|
46
81
|
require_relative "cabriolet/models/hlp_file"
|
|
82
|
+
require_relative "cabriolet/models/winhelp_header"
|
|
47
83
|
require_relative "cabriolet/models/lit_header"
|
|
48
84
|
require_relative "cabriolet/models/oab_header"
|
|
49
85
|
|
|
86
|
+
# Load errors first (needed by algorithm_factory)
|
|
87
|
+
|
|
88
|
+
# Load plugin system
|
|
89
|
+
require_relative "cabriolet/plugin"
|
|
90
|
+
require_relative "cabriolet/plugin_validator"
|
|
91
|
+
require_relative "cabriolet/plugin_manager"
|
|
92
|
+
|
|
93
|
+
# Load algorithm factory
|
|
94
|
+
require_relative "cabriolet/algorithm_factory"
|
|
95
|
+
|
|
96
|
+
# Load core components
|
|
97
|
+
|
|
98
|
+
require_relative "cabriolet/quantum_shared"
|
|
99
|
+
|
|
50
100
|
require_relative "cabriolet/huffman/tree"
|
|
51
101
|
require_relative "cabriolet/huffman/decoder"
|
|
52
102
|
require_relative "cabriolet/huffman/encoder"
|
|
@@ -85,6 +135,11 @@ require_relative "cabriolet/hlp/parser"
|
|
|
85
135
|
require_relative "cabriolet/hlp/decompressor"
|
|
86
136
|
require_relative "cabriolet/hlp/compressor"
|
|
87
137
|
|
|
138
|
+
require_relative "cabriolet/hlp/winhelp/parser"
|
|
139
|
+
require_relative "cabriolet/hlp/winhelp/zeck_lz77"
|
|
140
|
+
require_relative "cabriolet/hlp/winhelp/decompressor"
|
|
141
|
+
require_relative "cabriolet/hlp/winhelp/compressor"
|
|
142
|
+
|
|
88
143
|
require_relative "cabriolet/lit/decompressor"
|
|
89
144
|
require_relative "cabriolet/lit/compressor"
|
|
90
145
|
|
|
@@ -93,12 +148,118 @@ require_relative "cabriolet/oab/compressor"
|
|
|
93
148
|
|
|
94
149
|
# Load new advanced features
|
|
95
150
|
require_relative "cabriolet/format_detector"
|
|
96
|
-
require_relative "cabriolet/
|
|
151
|
+
require_relative "cabriolet/extraction/base_extractor"
|
|
152
|
+
require_relative "cabriolet/extraction/extractor"
|
|
97
153
|
require_relative "cabriolet/streaming"
|
|
98
154
|
require_relative "cabriolet/validator"
|
|
99
155
|
require_relative "cabriolet/repairer"
|
|
100
156
|
require_relative "cabriolet/modifier"
|
|
101
|
-
require_relative "cabriolet/parallel"
|
|
102
157
|
|
|
103
158
|
# Load CLI (optional, for command-line usage)
|
|
104
159
|
require_relative "cabriolet/cli"
|
|
160
|
+
|
|
161
|
+
# Convenience methods at top level
|
|
162
|
+
module Cabriolet
|
|
163
|
+
class << self
|
|
164
|
+
# Open and parse an archive with automatic format detection
|
|
165
|
+
#
|
|
166
|
+
# @param path [String] Path to the archive file
|
|
167
|
+
# @param options [Hash] Options to pass to the parser
|
|
168
|
+
# @return [Object] Parsed archive object
|
|
169
|
+
# @raise [UnsupportedFormatError] if format cannot be detected or is unsupported
|
|
170
|
+
#
|
|
171
|
+
# @example
|
|
172
|
+
# archive = Cabriolet.open('unknown.archive')
|
|
173
|
+
# archive.files.each { |f| puts f.name }
|
|
174
|
+
def open(path, **options)
|
|
175
|
+
parser_class = FormatDetector.parser_for(path)
|
|
176
|
+
|
|
177
|
+
unless parser_class
|
|
178
|
+
format = detect_format(path)
|
|
179
|
+
raise UnsupportedFormatError,
|
|
180
|
+
"Unable to detect format or no parser available for: #{path} (detected: #{format || 'unknown'})"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
parser_class.new(**options).parse(path)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Detect format of an archive file
|
|
187
|
+
#
|
|
188
|
+
# @param path [String] Path to the file
|
|
189
|
+
# @return [Symbol, nil] Detected format symbol or nil
|
|
190
|
+
#
|
|
191
|
+
# @example
|
|
192
|
+
# format = Cabriolet.detect_format('file.cab')
|
|
193
|
+
# # => :cab
|
|
194
|
+
def detect_format(path)
|
|
195
|
+
FormatDetector.detect(path)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Extract files from an archive with automatic format detection
|
|
199
|
+
#
|
|
200
|
+
# @param archive_path [String] Path to the archive
|
|
201
|
+
# @param output_dir [String] Directory to extract to
|
|
202
|
+
# @param options [Hash] Extraction options
|
|
203
|
+
# @option options [Integer] :workers (4) Number of parallel workers (1 = sequential)
|
|
204
|
+
# @option options [Boolean] :preserve_paths (true) Preserve directory structure
|
|
205
|
+
# @option options [Boolean] :overwrite (false) Overwrite existing files
|
|
206
|
+
# @return [Hash] Extraction statistics
|
|
207
|
+
#
|
|
208
|
+
# @example Sequential extraction
|
|
209
|
+
# Cabriolet.extract('archive.cab', 'output/')
|
|
210
|
+
#
|
|
211
|
+
# @example Parallel extraction with 8 workers
|
|
212
|
+
# stats = Cabriolet.extract('file.chm', 'docs/', workers: 8)
|
|
213
|
+
# puts "Extracted #{stats[:extracted]} files"
|
|
214
|
+
def extract(archive_path, output_dir, **options)
|
|
215
|
+
archive = open(archive_path)
|
|
216
|
+
extractor = Extraction::Extractor.new(archive, output_dir, **options)
|
|
217
|
+
extractor.extract_all
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Get information about an archive without full extraction
|
|
221
|
+
#
|
|
222
|
+
# @param path [String] Path to the archive
|
|
223
|
+
# @return [Hash] Archive information
|
|
224
|
+
#
|
|
225
|
+
# @example
|
|
226
|
+
# info = Cabriolet.info('archive.cab')
|
|
227
|
+
# # => { format: :cab, file_count: 145, total_size: 52428800, ... }
|
|
228
|
+
def info(path)
|
|
229
|
+
archive = open(path)
|
|
230
|
+
format = detect_format(path)
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
format: format,
|
|
234
|
+
path: path,
|
|
235
|
+
file_count: archive.files.count,
|
|
236
|
+
total_size: archive.files.sum { |f| f.size || 0 },
|
|
237
|
+
compressed_size: File.size(path),
|
|
238
|
+
compression_ratio: calculate_compression_ratio(archive, path),
|
|
239
|
+
files: archive.files.map { |f| file_info(f) },
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
def calculate_compression_ratio(archive, path)
|
|
246
|
+
total_uncompressed = archive.files.sum { |f| f.size || 0 }
|
|
247
|
+
compressed = File.size(path)
|
|
248
|
+
|
|
249
|
+
return 0 if total_uncompressed.zero?
|
|
250
|
+
|
|
251
|
+
((compressed.to_f / total_uncompressed) * 100).round(2)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def file_info(file)
|
|
255
|
+
{
|
|
256
|
+
name: file.name,
|
|
257
|
+
size: file.size,
|
|
258
|
+
compressed_size: file.respond_to?(:compressed_size) ? file.compressed_size : nil,
|
|
259
|
+
attributes: file.respond_to?(:attributes) ? file.attributes : nil,
|
|
260
|
+
date: file.respond_to?(:date) ? file.date : nil,
|
|
261
|
+
time: file.respond_to?(:time) ? file.time : nil,
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|