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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +703 -38
  3. data/lib/cabriolet/algorithm_factory.rb +250 -0
  4. data/lib/cabriolet/base_compressor.rb +206 -0
  5. data/lib/cabriolet/binary/bitstream.rb +167 -16
  6. data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
  7. data/lib/cabriolet/binary/chm_structures.rb +2 -2
  8. data/lib/cabriolet/binary/hlp_structures.rb +258 -37
  9. data/lib/cabriolet/binary/lit_structures.rb +231 -65
  10. data/lib/cabriolet/binary/oab_structures.rb +17 -1
  11. data/lib/cabriolet/cab/command_handler.rb +226 -0
  12. data/lib/cabriolet/cab/compressor.rb +108 -84
  13. data/lib/cabriolet/cab/decompressor.rb +16 -20
  14. data/lib/cabriolet/cab/extractor.rb +142 -66
  15. data/lib/cabriolet/cab/file_compression_work.rb +52 -0
  16. data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
  17. data/lib/cabriolet/checksum.rb +49 -0
  18. data/lib/cabriolet/chm/command_handler.rb +227 -0
  19. data/lib/cabriolet/chm/compressor.rb +7 -3
  20. data/lib/cabriolet/chm/decompressor.rb +39 -21
  21. data/lib/cabriolet/chm/parser.rb +5 -2
  22. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  23. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  24. data/lib/cabriolet/cli/command_registry.rb +83 -0
  25. data/lib/cabriolet/cli.rb +356 -607
  26. data/lib/cabriolet/collections/file_collection.rb +175 -0
  27. data/lib/cabriolet/compressors/base.rb +1 -1
  28. data/lib/cabriolet/compressors/lzx.rb +241 -54
  29. data/lib/cabriolet/compressors/mszip.rb +35 -3
  30. data/lib/cabriolet/compressors/quantum.rb +36 -95
  31. data/lib/cabriolet/decompressors/base.rb +1 -1
  32. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  33. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  34. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  35. data/lib/cabriolet/decompressors/quantum.rb +83 -53
  36. data/lib/cabriolet/errors.rb +3 -0
  37. data/lib/cabriolet/extraction/base_extractor.rb +88 -0
  38. data/lib/cabriolet/extraction/extractor.rb +171 -0
  39. data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
  40. data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
  41. data/lib/cabriolet/file_entry.rb +156 -0
  42. data/lib/cabriolet/file_manager.rb +144 -0
  43. data/lib/cabriolet/format_base.rb +79 -0
  44. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  45. data/lib/cabriolet/hlp/compressor.rb +28 -238
  46. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  47. data/lib/cabriolet/hlp/parser.rb +52 -101
  48. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  49. data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
  50. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  51. data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
  52. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  53. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  54. data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
  55. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  56. data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
  57. data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
  58. data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
  59. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  60. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  61. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  62. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  63. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  64. data/lib/cabriolet/huffman/encoder.rb +15 -12
  65. data/lib/cabriolet/huffman/tree.rb +85 -1
  66. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  67. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  68. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  69. data/lib/cabriolet/lit/command_handler.rb +221 -0
  70. data/lib/cabriolet/lit/compressor.rb +119 -168
  71. data/lib/cabriolet/lit/content_encoder.rb +76 -0
  72. data/lib/cabriolet/lit/content_type_detector.rb +50 -0
  73. data/lib/cabriolet/lit/decompressor.rb +518 -152
  74. data/lib/cabriolet/lit/directory_builder.rb +153 -0
  75. data/lib/cabriolet/lit/guid_generator.rb +16 -0
  76. data/lib/cabriolet/lit/header_writer.rb +124 -0
  77. data/lib/cabriolet/lit/parser.rb +670 -0
  78. data/lib/cabriolet/lit/piece_builder.rb +74 -0
  79. data/lib/cabriolet/lit/structure_builder.rb +252 -0
  80. data/lib/cabriolet/models/hlp_file.rb +130 -29
  81. data/lib/cabriolet/models/hlp_header.rb +105 -17
  82. data/lib/cabriolet/models/lit_header.rb +212 -25
  83. data/lib/cabriolet/models/szdd_header.rb +10 -2
  84. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  85. data/lib/cabriolet/oab/command_handler.rb +257 -0
  86. data/lib/cabriolet/oab/compressor.rb +17 -8
  87. data/lib/cabriolet/oab/decompressor.rb +41 -10
  88. data/lib/cabriolet/offset_calculator.rb +81 -0
  89. data/lib/cabriolet/plugin.rb +233 -0
  90. data/lib/cabriolet/plugin_manager.rb +453 -0
  91. data/lib/cabriolet/plugin_validator.rb +422 -0
  92. data/lib/cabriolet/quantum_shared.rb +105 -0
  93. data/lib/cabriolet/system/io_system.rb +3 -0
  94. data/lib/cabriolet/system/memory_handle.rb +17 -4
  95. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  96. data/lib/cabriolet/szdd/compressor.rb +15 -11
  97. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  98. data/lib/cabriolet/version.rb +1 -1
  99. data/lib/cabriolet.rb +181 -20
  100. metadata +69 -4
  101. data/lib/cabriolet/auto.rb +0 -173
  102. 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
- def initialize(io_system = nil)
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 = Compressors::LZSS.new(
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
- lzss_mode = if format == :normal
111
- Compressors::LZSS::MODE_EXPAND
112
- else
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
- lzss_mode,
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
- def initialize(io_system = nil)
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 = Decompressors::LZSS.new(
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(header.length)
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 = Decompressors::LZSS.new(
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(header.length)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cabriolet
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.1"
5
5
  end
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
- self.default_buffer_size = 4096
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
- # Load core components
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/auto"
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