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
@@ -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
- def initialize(io_system = nil)
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.fetch(:block_size, @block_size)
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.fetch(:block_size, @block_size)
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.fetch(:block_size, @block_size)
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
- patch_data = compressed_data && compressed_data.bytesize < block_data.bytesize ? compressed_data : block_data
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 = Compressors::LZX.new(
291
- @io_system, input_mem, output_mem, @buffer_size,
292
- window_bits: window_bits
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
- def initialize(io_system = nil)
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, 16)
166
- if block_data.length < 16
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 = Decompressors::LZX.new(
249
- @io_system, input_mem, output_mem, @buffer_size,
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 = Decompressors::LZX.new(
290
- @io_system, input_mem, output_mem, @buffer_size,
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
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ # Base class for all Cabriolet plugins
5
+ #
6
+ # Plugins extend Cabriolet's functionality by providing custom compression
7
+ # algorithms, format handlers, or other enhancements. All plugins must
8
+ # inherit from this base class and implement required methods.
9
+ #
10
+ # @abstract Subclass and implement {#metadata} and {#setup} to create
11
+ # a plugin
12
+ #
13
+ # @example Creating a simple plugin
14
+ # class MyPlugin < Cabriolet::Plugin
15
+ # def metadata
16
+ # {
17
+ # name: "my-plugin",
18
+ # version: "1.0.0",
19
+ # author: "Your Name",
20
+ # description: "Adds custom compression algorithm",
21
+ # cabriolet_version: "~> 0.1"
22
+ # }
23
+ # end
24
+ #
25
+ # def setup
26
+ # register_algorithm(:custom, CustomAlgorithm,
27
+ # category: :compressor)
28
+ # end
29
+ # end
30
+ class Plugin
31
+ # Plugin states
32
+ STATES = %i[discovered loaded active failed disabled].freeze
33
+
34
+ # @return [Symbol] Current plugin state
35
+ attr_reader :state
36
+
37
+ # Initialize a new plugin
38
+ #
39
+ # @param manager [PluginManager] The plugin manager instance
40
+ def initialize(manager = nil)
41
+ @manager = manager
42
+ @state = :discovered
43
+ end
44
+
45
+ # Get plugin metadata
46
+ #
47
+ # @abstract Must be implemented by subclasses
48
+ #
49
+ # @return [Hash] Plugin metadata containing:
50
+ # @option return [String] :name Plugin name (required)
51
+ # @option return [String] :version Plugin version (required)
52
+ # @option return [String] :author Plugin author (required)
53
+ # @option return [String] :description Plugin description (required)
54
+ # @option return [String] :cabriolet_version Compatible Cabriolet
55
+ # version (required)
56
+ # @option return [String] :homepage Plugin homepage URL (optional)
57
+ # @option return [String] :license Plugin license (optional)
58
+ # @option return [Array<String>] :dependencies Plugin dependencies
59
+ # (optional)
60
+ # @option return [Array<String>] :tags Search tags (optional)
61
+ # @option return [Hash] :provides What the plugin provides (optional)
62
+ #
63
+ # @raise [NotImplementedError] If not implemented by subclass
64
+ #
65
+ # @example Minimal metadata
66
+ # def metadata
67
+ # {
68
+ # name: "my-plugin",
69
+ # version: "1.0.0",
70
+ # author: "Your Name",
71
+ # description: "Plugin description",
72
+ # cabriolet_version: "~> 0.1"
73
+ # }
74
+ # end
75
+ #
76
+ # @example Full metadata
77
+ # def metadata
78
+ # {
79
+ # name: "advanced-plugin",
80
+ # version: "2.0.0",
81
+ # author: "Developer",
82
+ # description: "Advanced features",
83
+ # cabriolet_version: ">= 0.1.0",
84
+ # homepage: "https://example.com",
85
+ # license: "MIT",
86
+ # dependencies: ["other-plugin >= 1.0"],
87
+ # tags: ["compression", "algorithm"],
88
+ # provides: { algorithms: [:custom], formats: [:special] }
89
+ # }
90
+ # end
91
+ def metadata
92
+ raise NotImplementedError,
93
+ "#{self.class} must implement metadata method"
94
+ end
95
+
96
+ # Setup the plugin
97
+ #
98
+ # Called when the plugin is loaded. Use this method to register
99
+ # algorithms, formats, or perform other initialization tasks.
100
+ #
101
+ # @abstract Must be implemented by subclasses
102
+ #
103
+ # @raise [NotImplementedError] If not implemented by subclass
104
+ #
105
+ # @example Register an algorithm
106
+ # def setup
107
+ # register_algorithm(:myalgo, MyAlgorithm,
108
+ # category: :compressor)
109
+ # end
110
+ #
111
+ # @example Register multiple items
112
+ # def setup
113
+ # register_algorithm(:algo1, Algo1, category: :compressor)
114
+ # register_algorithm(:algo2, Algo2, category: :decompressor)
115
+ # register_format(:myformat, MyFormatHandler)
116
+ # end
117
+ def setup
118
+ raise NotImplementedError,
119
+ "#{self.class} must implement setup method"
120
+ end
121
+
122
+ # Activate the plugin
123
+ #
124
+ # Called when the plugin is activated. Override to perform actions
125
+ # when the plugin becomes active.
126
+ #
127
+ # @return [void]
128
+ #
129
+ # @example Add hooks on activation
130
+ # def activate
131
+ # puts "#{metadata[:name]} activated"
132
+ # # Additional activation logic...
133
+ # end
134
+ def activate
135
+ # Default implementation does nothing
136
+ end
137
+
138
+ # Deactivate the plugin
139
+ #
140
+ # Called when the plugin is deactivated. Override to perform cleanup
141
+ # when the plugin is deactivated.
142
+ #
143
+ # @return [void]
144
+ #
145
+ # @example Cleanup on deactivation
146
+ # def deactivate
147
+ # # Cleanup resources...
148
+ # puts "#{metadata[:name]} deactivated"
149
+ # end
150
+ def deactivate
151
+ # Default implementation does nothing
152
+ end
153
+
154
+ # Cleanup the plugin
155
+ #
156
+ # Called when the plugin is unloaded. Override to perform final
157
+ # cleanup tasks.
158
+ #
159
+ # @return [void]
160
+ #
161
+ # @example Final cleanup
162
+ # def cleanup
163
+ # # Release resources...
164
+ # # Close connections...
165
+ # end
166
+ def cleanup
167
+ # Default implementation does nothing
168
+ end
169
+
170
+ protected
171
+
172
+ # Register a compression or decompression algorithm
173
+ #
174
+ # @param type [Symbol] Algorithm type identifier
175
+ # @param klass [Class] Algorithm class
176
+ # @param options [Hash] Registration options
177
+ # @option options [Symbol] :category Required - :compressor or
178
+ # :decompressor
179
+ # @option options [Integer] :priority Algorithm priority (default: 0)
180
+ # @option options [Symbol, nil] :format Format restriction (optional)
181
+ #
182
+ # @return [void]
183
+ #
184
+ # @raise [PluginError] If manager is not available
185
+ #
186
+ # @example Register a compressor
187
+ # register_algorithm(:myalgo, MyCompressor,
188
+ # category: :compressor, priority: 10)
189
+ #
190
+ # @example Register a format-specific decompressor
191
+ # register_algorithm(:special, SpecialDecompressor,
192
+ # category: :decompressor, format: :cab)
193
+ def register_algorithm(type, klass, **options)
194
+ raise PluginError, "Plugin manager not available" unless @manager
195
+
196
+ Cabriolet.algorithm_factory.register(type, klass, **options)
197
+ end
198
+
199
+ # Register a format handler
200
+ #
201
+ # @param format [Symbol] Format identifier
202
+ # @param handler [Class] Format handler class
203
+ #
204
+ # @return [void]
205
+ #
206
+ # @raise [PluginError] If manager is not available
207
+ #
208
+ # @example Register a format handler
209
+ # register_format(:myformat, MyFormatHandler)
210
+ def register_format(format, handler)
211
+ raise PluginError, "Plugin manager not available" unless @manager
212
+
213
+ # Format registration will be implemented when format registry exists
214
+ # For now, store in manager's format registry
215
+ @manager.register_format(format, handler)
216
+ end
217
+
218
+ # Update plugin state
219
+ #
220
+ # @param new_state [Symbol] New state (must be in STATES)
221
+ #
222
+ # @return [void]
223
+ #
224
+ # @raise [ArgumentError] If state is invalid
225
+ def update_state(new_state)
226
+ unless STATES.include?(new_state)
227
+ raise ArgumentError, "Invalid state: #{new_state}"
228
+ end
229
+
230
+ @state = new_state
231
+ end
232
+ end
233
+ end