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,453 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "yaml"
5
+
6
+ module Cabriolet
7
+ # Manages plugin lifecycle and registry for Cabriolet
8
+ #
9
+ # The PluginManager is a thread-safe singleton that handles plugin
10
+ # discovery, loading, activation, and deactivation. It maintains plugin
11
+ # states and resolves dependencies.
12
+ #
13
+ # @example Access the plugin manager
14
+ # manager = Cabriolet::PluginManager.instance
15
+ # manager.discover_plugins
16
+ # manager.load_plugin("my-plugin")
17
+ # manager.activate_plugin("my-plugin")
18
+ #
19
+ # @example Using global accessor
20
+ # Cabriolet.plugin_manager.discover_plugins
21
+ # Cabriolet.plugin_manager.list_plugins(state: :active)
22
+ class PluginManager
23
+ include Singleton
24
+
25
+ # @return [Hash] Plugin registry by name
26
+ attr_reader :plugins
27
+
28
+ # @return [Hash] Format registry
29
+ attr_reader :formats
30
+
31
+ # Initialize the plugin manager
32
+ def initialize
33
+ @plugins = {}
34
+ @formats = {}
35
+ @mutex = Mutex.new
36
+ @config = load_config
37
+ end
38
+
39
+ # Discover available plugins
40
+ #
41
+ # Searches for plugins in gem paths using the pattern
42
+ # 'cabriolet/plugins/**/*.rb'. Discovered plugins are added to the
43
+ # registry in :discovered state.
44
+ #
45
+ # @return [Array<String>] List of discovered plugin names
46
+ #
47
+ # @example Discover plugins
48
+ # manager.discover_plugins
49
+ # #=> ["plugin1", "plugin2"]
50
+ def discover_plugins
51
+ @mutex.synchronize do
52
+ plugin_files = Gem.find_files("cabriolet/plugins/**/*.rb")
53
+
54
+ plugin_files.each do |file|
55
+ load_plugin_file(file)
56
+ rescue StandardError => e
57
+ warn "Failed to load plugin from #{file}: #{e.message}"
58
+ end
59
+
60
+ @plugins.keys
61
+ end
62
+ end
63
+
64
+ # Register a plugin instance
65
+ #
66
+ # Adds a plugin to the registry. The plugin must be a valid Plugin
67
+ # instance with proper metadata.
68
+ #
69
+ # @param plugin_instance [Plugin] Plugin instance to register
70
+ #
71
+ # @return [Boolean] True if registered successfully
72
+ #
73
+ # @raise [PluginError] If plugin is invalid
74
+ #
75
+ # @example Register a plugin
76
+ # plugin = MyPlugin.new(manager)
77
+ # manager.register(plugin)
78
+ def register(plugin_instance)
79
+ @mutex.synchronize do
80
+ unless plugin_instance.is_a?(Plugin)
81
+ raise PluginError,
82
+ "Plugin must inherit from Cabriolet::Plugin"
83
+ end
84
+
85
+ # Validate plugin
86
+ validation = PluginValidator.validate(plugin_instance.class)
87
+ unless validation[:valid]
88
+ raise PluginError,
89
+ "Plugin validation failed: #{validation[:errors].join(', ')}"
90
+ end
91
+
92
+ meta = plugin_instance.metadata
93
+ name = meta[:name]
94
+
95
+ if @plugins.key?(name)
96
+ raise PluginError, "Plugin '#{name}' already registered"
97
+ end
98
+
99
+ @plugins[name] = {
100
+ instance: plugin_instance,
101
+ metadata: meta,
102
+ state: :discovered,
103
+ }
104
+
105
+ true
106
+ end
107
+ end
108
+
109
+ # Load a plugin
110
+ #
111
+ # Loads and validates a discovered plugin. Calls the plugin's setup
112
+ # method and transitions to :loaded state.
113
+ #
114
+ # @param name [String] Plugin name
115
+ #
116
+ # @return [Boolean] True if loaded successfully
117
+ #
118
+ # @raise [PluginError] If plugin not found or load fails
119
+ #
120
+ # @example Load a plugin
121
+ # manager.load_plugin("my-plugin")
122
+ def load_plugin(name)
123
+ @mutex.synchronize do
124
+ entry = @plugins[name]
125
+ raise PluginError, "Plugin '#{name}' not found" unless entry
126
+
127
+ if %i[loaded active].include?(entry[:state])
128
+ return true
129
+ end
130
+
131
+ begin
132
+ plugin = entry[:instance]
133
+
134
+ # Check dependencies
135
+ check_dependencies!(entry[:metadata])
136
+
137
+ # Call setup
138
+ plugin.setup
139
+ plugin.send(:update_state, :loaded)
140
+ entry[:state] = :loaded
141
+
142
+ true
143
+ rescue StandardError => e
144
+ plugin.send(:update_state, :failed)
145
+ entry[:state] = :failed
146
+ entry[:error] = e.message
147
+ raise PluginError, "Failed to load plugin '#{name}': #{e.message}"
148
+ end
149
+ end
150
+ end
151
+
152
+ # Activate a plugin
153
+ #
154
+ # Activates a loaded plugin. Calls the plugin's activate method and
155
+ # transitions to :active state.
156
+ #
157
+ # @param name [String] Plugin name
158
+ #
159
+ # @return [Boolean] True if activated successfully
160
+ #
161
+ # @raise [PluginError] If plugin not found or not loaded
162
+ #
163
+ # @example Activate a plugin
164
+ # manager.activate_plugin("my-plugin")
165
+ def activate_plugin(name)
166
+ @mutex.synchronize do
167
+ entry = @plugins[name]
168
+ raise PluginError, "Plugin '#{name}' not found" unless entry
169
+
170
+ if entry[:state] == :active
171
+ return true
172
+ end
173
+
174
+ unless entry[:state] == :loaded
175
+ raise PluginError,
176
+ "Plugin '#{name}' must be loaded before activation"
177
+ end
178
+
179
+ begin
180
+ plugin = entry[:instance]
181
+ plugin.activate
182
+ plugin.send(:update_state, :active)
183
+ entry[:state] = :active
184
+
185
+ true
186
+ rescue StandardError => e
187
+ plugin.send(:update_state, :failed)
188
+ entry[:state] = :failed
189
+ entry[:error] = e.message
190
+ raise PluginError,
191
+ "Failed to activate plugin '#{name}': #{e.message}"
192
+ end
193
+ end
194
+ end
195
+
196
+ # Deactivate a plugin
197
+ #
198
+ # Deactivates an active plugin. Calls the plugin's deactivate method
199
+ # and transitions back to :loaded state.
200
+ #
201
+ # @param name [String] Plugin name
202
+ #
203
+ # @return [Boolean] True if deactivated successfully
204
+ #
205
+ # @raise [PluginError] If plugin not found
206
+ #
207
+ # @example Deactivate a plugin
208
+ # manager.deactivate_plugin("my-plugin")
209
+ def deactivate_plugin(name)
210
+ @mutex.synchronize do
211
+ entry = @plugins[name]
212
+ raise PluginError, "Plugin '#{name}' not found" unless entry
213
+
214
+ if entry[:state] != :active
215
+ return true
216
+ end
217
+
218
+ begin
219
+ plugin = entry[:instance]
220
+ plugin.deactivate
221
+ plugin.send(:update_state, :loaded)
222
+ entry[:state] = :loaded
223
+
224
+ true
225
+ rescue StandardError => e
226
+ entry[:error] = e.message
227
+ raise PluginError,
228
+ "Failed to deactivate plugin '#{name}': #{e.message}"
229
+ end
230
+ end
231
+ end
232
+
233
+ # List plugins
234
+ #
235
+ # Returns plugin information, optionally filtered by state.
236
+ #
237
+ # @param state [Symbol, nil] Optional state filter (:discovered,
238
+ # :loaded, :active, :failed, :disabled)
239
+ #
240
+ # @return [Hash] Plugin information keyed by name
241
+ #
242
+ # @example List all plugins
243
+ # manager.list_plugins
244
+ # #=> { "plugin1" => {...}, "plugin2" => {...} }
245
+ #
246
+ # @example List only active plugins
247
+ # manager.list_plugins(state: :active)
248
+ # #=> { "active-plugin" => {...} }
249
+ def list_plugins(state: nil)
250
+ @mutex.synchronize do
251
+ if state.nil?
252
+ @plugins.transform_values do |entry|
253
+ {
254
+ metadata: entry[:metadata],
255
+ state: entry[:state],
256
+ error: entry[:error],
257
+ }
258
+ end
259
+ else
260
+ @plugins.select { |_, entry| entry[:state] == state }
261
+ .transform_values do |entry|
262
+ {
263
+ metadata: entry[:metadata],
264
+ state: entry[:state],
265
+ error: entry[:error],
266
+ }
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ # Get a plugin by name
273
+ #
274
+ # @param name [String] Plugin name
275
+ #
276
+ # @return [Plugin, nil] Plugin instance or nil if not found
277
+ #
278
+ # @example Get a plugin
279
+ # plugin = manager.plugin("my-plugin")
280
+ def plugin(name)
281
+ @mutex.synchronize do
282
+ @plugins[name]&.dig(:instance)
283
+ end
284
+ end
285
+
286
+ # Check if a plugin is active
287
+ #
288
+ # @param name [String] Plugin name
289
+ #
290
+ # @return [Boolean] True if plugin is active
291
+ #
292
+ # @example Check plugin status
293
+ # manager.plugin_active?("my-plugin") #=> true
294
+ def plugin_active?(name)
295
+ @mutex.synchronize do
296
+ @plugins[name]&.dig(:state) == :active
297
+ end
298
+ end
299
+
300
+ # Register a format handler
301
+ #
302
+ # Called by plugins to register format handlers. This is used
303
+ # internally by Plugin#register_format.
304
+ #
305
+ # @param format [Symbol] Format identifier
306
+ # @param handler [Class] Handler class
307
+ #
308
+ # @return [void]
309
+ #
310
+ # @api private
311
+ def register_format(format, handler)
312
+ @mutex.synchronize do
313
+ @formats[format] = handler
314
+ end
315
+ end
316
+
317
+ # Get format handler
318
+ #
319
+ # @param format [Symbol] Format identifier
320
+ #
321
+ # @return [Class, nil] Handler class or nil
322
+ #
323
+ # @example Get format handler
324
+ # handler = manager.format_handler(:myformat)
325
+ def format_handler(format)
326
+ @mutex.synchronize do
327
+ @formats[format]
328
+ end
329
+ end
330
+
331
+ private
332
+
333
+ # Load configuration from ~/.cabriolet/plugins.yml
334
+ #
335
+ # @return [Hash] Configuration hash
336
+ def load_config
337
+ config_path = File.expand_path("~/.cabriolet/plugins.yml")
338
+ return {} unless File.exist?(config_path)
339
+
340
+ YAML.load_file(config_path) || {}
341
+ rescue StandardError => e
342
+ warn "Failed to load plugin config: #{e.message}"
343
+ {}
344
+ end
345
+
346
+ # Load a plugin file
347
+ #
348
+ # @param file [String] Plugin file path
349
+ #
350
+ # @return [void]
351
+ def load_plugin_file(file)
352
+ require file
353
+
354
+ # After requiring, plugin classes should auto-register
355
+ # This is a convention - plugins call register in their class body
356
+ end
357
+
358
+ # Check plugin dependencies
359
+ #
360
+ # @param metadata [Hash] Plugin metadata
361
+ #
362
+ # @return [void]
363
+ #
364
+ # @raise [PluginError] If dependencies not met
365
+ def check_dependencies!(metadata)
366
+ dependencies = metadata[:dependencies] || []
367
+
368
+ dependencies.each do |dep|
369
+ dep_name, dep_version = parse_dependency(dep)
370
+
371
+ unless @plugins.key?(dep_name)
372
+ raise PluginError,
373
+ "Missing dependency: #{dep_name}"
374
+ end
375
+
376
+ dep_entry = @plugins[dep_name]
377
+ unless %i[loaded active].include?(dep_entry[:state])
378
+ raise PluginError,
379
+ "Dependency '#{dep_name}' not loaded"
380
+ end
381
+
382
+ if dep_version
383
+ actual_version = dep_entry[:metadata][:version]
384
+ unless version_satisfies?(actual_version, dep_version)
385
+ raise PluginError,
386
+ "Dependency '#{dep_name}' version mismatch: " \
387
+ "need #{dep_version}, have #{actual_version}"
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+ # Parse dependency string
394
+ #
395
+ # @param dep [String] Dependency string (e.g., "plugin >= 1.0")
396
+ #
397
+ # @return [Array<String, String>] [name, version_requirement]
398
+ def parse_dependency(dep)
399
+ parts = dep.split
400
+ name = parts[0]
401
+ version = parts[1..].join(" ") if parts.length > 1
402
+
403
+ [name, version]
404
+ end
405
+
406
+ # Check if version satisfies requirement
407
+ #
408
+ # @param version [String] Actual version
409
+ # @param requirement [String] Version requirement
410
+ #
411
+ # @return [Boolean] True if satisfied
412
+ def version_satisfies?(version, requirement)
413
+ # Simple version check - can be enhanced with gem version logic
414
+ return true if requirement.nil?
415
+
416
+ # Parse requirement (e.g., ">= 1.0", "~> 2.0", "= 1.5")
417
+ if requirement.start_with?(">=")
418
+ min_version = requirement.sub(">=", "").strip
419
+ compare_versions(version, min_version) >= 0
420
+ elsif requirement.start_with?("~>")
421
+ # Pessimistic version constraint
422
+ base = requirement.sub("~>", "").strip
423
+ compare_versions(version, base) >= 0
424
+ elsif requirement.start_with?("=")
425
+ exact = requirement.sub("=", "").strip
426
+ version == exact
427
+ else
428
+ true
429
+ end
430
+ end
431
+
432
+ # Compare two version strings
433
+ #
434
+ # @param v1 [String] Version 1
435
+ # @param v2 [String] Version 2
436
+ #
437
+ # @return [Integer] -1, 0, or 1
438
+ def compare_versions(v1, v2)
439
+ parts1 = v1.split(".").map(&:to_i)
440
+ parts2 = v2.split(".").map(&:to_i)
441
+
442
+ [parts1.length, parts2.length].max.times do |i|
443
+ p1 = parts1[i] || 0
444
+ p2 = parts2[i] || 0
445
+
446
+ return -1 if p1 < p2
447
+ return 1 if p1 > p2
448
+ end
449
+
450
+ 0
451
+ end
452
+ end
453
+ end