cabriolet 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +700 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +154 -14
- data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +35 -43
- data/lib/cabriolet/cab/decompressor.rb +14 -19
- data/lib/cabriolet/cab/extractor.rb +140 -31
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +34 -45
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +3 -2
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +633 -38
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +67 -17
- metadata +33 -2
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
# Validates plugin classes and configurations
|
|
5
|
+
#
|
|
6
|
+
# The PluginValidator provides comprehensive validation for plugins
|
|
7
|
+
# including inheritance checks, metadata validation, version
|
|
8
|
+
# compatibility, and safety scanning.
|
|
9
|
+
#
|
|
10
|
+
# @example Validate a plugin class
|
|
11
|
+
# result = PluginValidator.validate(MyPlugin)
|
|
12
|
+
# if result[:valid]
|
|
13
|
+
# puts "Plugin is valid"
|
|
14
|
+
# else
|
|
15
|
+
# puts "Errors: #{result[:errors].join(', ')}"
|
|
16
|
+
# end
|
|
17
|
+
class PluginValidator
|
|
18
|
+
# Required metadata fields
|
|
19
|
+
REQUIRED_METADATA = %i[name version author description
|
|
20
|
+
cabriolet_version].freeze
|
|
21
|
+
|
|
22
|
+
# Dangerous method names to check for
|
|
23
|
+
DANGEROUS_METHODS = %w[
|
|
24
|
+
system exec spawn ` fork eval instance_eval class_eval
|
|
25
|
+
module_eval binding const_set remove_const send __send__
|
|
26
|
+
method_missing respond_to_missing?
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Validate a plugin class
|
|
31
|
+
#
|
|
32
|
+
# Performs comprehensive validation including inheritance, metadata,
|
|
33
|
+
# version compatibility, and safety checks.
|
|
34
|
+
#
|
|
35
|
+
# @param plugin_class [Class] Plugin class to validate
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash] Validation result with:
|
|
38
|
+
# - :valid [Boolean] True if all checks pass
|
|
39
|
+
# - :errors [Array<String>] List of validation errors (empty if
|
|
40
|
+
# valid)
|
|
41
|
+
# - :warnings [Array<String>] List of warnings (non-fatal issues)
|
|
42
|
+
#
|
|
43
|
+
# @example Validate a plugin
|
|
44
|
+
# result = PluginValidator.validate(MyPlugin)
|
|
45
|
+
# result[:valid] #=> true
|
|
46
|
+
# result[:errors] #=> []
|
|
47
|
+
# result[:warnings] #=> ["Uses eval in setup method"]
|
|
48
|
+
def validate(plugin_class)
|
|
49
|
+
errors = []
|
|
50
|
+
warnings = []
|
|
51
|
+
|
|
52
|
+
# Check inheritance
|
|
53
|
+
inherit_errors = validate_inheritance(plugin_class)
|
|
54
|
+
errors.concat(inherit_errors)
|
|
55
|
+
|
|
56
|
+
# If inheritance fails, stop here
|
|
57
|
+
return { valid: false, errors: errors, warnings: warnings } unless
|
|
58
|
+
inherit_errors.empty?
|
|
59
|
+
|
|
60
|
+
# Create instance to check metadata
|
|
61
|
+
begin
|
|
62
|
+
instance = plugin_class.new(nil)
|
|
63
|
+
metadata = instance.metadata
|
|
64
|
+
|
|
65
|
+
# Validate metadata
|
|
66
|
+
meta_errors = validate_metadata(metadata)
|
|
67
|
+
errors.concat(meta_errors)
|
|
68
|
+
|
|
69
|
+
# Check version compatibility
|
|
70
|
+
if metadata[:cabriolet_version]
|
|
71
|
+
version_errors = validate_version_compatibility(
|
|
72
|
+
metadata[:cabriolet_version],
|
|
73
|
+
Cabriolet::VERSION,
|
|
74
|
+
)
|
|
75
|
+
errors.concat(version_errors)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Validate dependencies
|
|
79
|
+
if metadata[:dependencies]
|
|
80
|
+
dep_warnings = validate_dependencies(metadata[:dependencies])
|
|
81
|
+
warnings.concat(dep_warnings)
|
|
82
|
+
end
|
|
83
|
+
rescue NotImplementedError => e
|
|
84
|
+
errors << "Plugin does not implement required method: " \
|
|
85
|
+
"#{e.message}"
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
errors << "Failed to instantiate plugin: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Safety checks
|
|
91
|
+
safety_warnings = check_safety(plugin_class)
|
|
92
|
+
warnings.concat(safety_warnings)
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
valid: errors.empty?,
|
|
96
|
+
errors: errors,
|
|
97
|
+
warnings: warnings,
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Validate plugin inheritance
|
|
102
|
+
#
|
|
103
|
+
# Checks that the plugin class properly inherits from
|
|
104
|
+
# Cabriolet::Plugin.
|
|
105
|
+
#
|
|
106
|
+
# @param plugin_class [Class] Plugin class to validate
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<String>] List of inheritance errors (empty if valid)
|
|
109
|
+
#
|
|
110
|
+
# @example Valid inheritance
|
|
111
|
+
# PluginValidator.validate_inheritance(MyPlugin)
|
|
112
|
+
# #=> []
|
|
113
|
+
#
|
|
114
|
+
# @example Invalid inheritance
|
|
115
|
+
# PluginValidator.validate_inheritance(Object)
|
|
116
|
+
# #=> ["Plugin must inherit from Cabriolet::Plugin"]
|
|
117
|
+
def validate_inheritance(plugin_class)
|
|
118
|
+
errors = []
|
|
119
|
+
|
|
120
|
+
unless plugin_class.is_a?(Class)
|
|
121
|
+
errors << "Plugin must be a class, got #{plugin_class.class}"
|
|
122
|
+
return errors
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
unless plugin_class < Plugin
|
|
126
|
+
errors << "Plugin must inherit from Cabriolet::Plugin"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
errors
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Validate plugin metadata
|
|
133
|
+
#
|
|
134
|
+
# Checks that all required metadata fields are present and valid.
|
|
135
|
+
#
|
|
136
|
+
# @param metadata [Hash] Plugin metadata to validate
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<String>] List of metadata errors (empty if valid)
|
|
139
|
+
#
|
|
140
|
+
# @example Valid metadata
|
|
141
|
+
# meta = { name: "test", version: "1.0", ... }
|
|
142
|
+
# PluginValidator.validate_metadata(meta)
|
|
143
|
+
# #=> []
|
|
144
|
+
#
|
|
145
|
+
# @example Missing fields
|
|
146
|
+
# PluginValidator.validate_metadata({})
|
|
147
|
+
# #=> ["Missing required metadata: name, version, ..."]
|
|
148
|
+
def validate_metadata(metadata)
|
|
149
|
+
errors = []
|
|
150
|
+
|
|
151
|
+
unless metadata.is_a?(Hash)
|
|
152
|
+
errors << "Metadata must be a Hash"
|
|
153
|
+
return errors
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Check required fields
|
|
157
|
+
missing = REQUIRED_METADATA - metadata.keys
|
|
158
|
+
unless missing.empty?
|
|
159
|
+
errors << "Missing required metadata: #{missing.join(', ')}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Validate field types and formats
|
|
163
|
+
if metadata[:name]
|
|
164
|
+
unless metadata[:name].is_a?(String) &&
|
|
165
|
+
!metadata[:name].empty?
|
|
166
|
+
errors << "Plugin name must be a non-empty string"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if metadata[:name].is_a?(String) && metadata[:name] =~ /^[a-z0-9_-]+$/
|
|
170
|
+
# Valid format - do nothing
|
|
171
|
+
elsif metadata[:name].is_a?(String)
|
|
172
|
+
errors << "Plugin name must contain only lowercase letters, " \
|
|
173
|
+
"numbers, hyphens, and underscores"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if metadata[:version] && !valid_version?(metadata[:version])
|
|
178
|
+
errors << "Plugin version must be a valid semantic version " \
|
|
179
|
+
"(e.g., '1.0.0')"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if metadata[:author] && !(metadata[:author].is_a?(String) &&
|
|
183
|
+
!metadata[:author].empty?)
|
|
184
|
+
errors << "Plugin author must be a non-empty string"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if metadata[:description] && !(metadata[:description].is_a?(String) &&
|
|
188
|
+
!metadata[:description].empty?)
|
|
189
|
+
errors << "Plugin description must be a non-empty string"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Optional fields validation
|
|
193
|
+
if metadata[:homepage] && !metadata[:homepage].empty? && !valid_url?(metadata[:homepage])
|
|
194
|
+
errors << "Plugin homepage must be a valid URL"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if metadata[:dependencies] && !metadata[:dependencies].is_a?(Array)
|
|
198
|
+
errors << "Plugin dependencies must be an array"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if metadata[:tags] && !metadata[:tags].is_a?(Array)
|
|
202
|
+
errors << "Plugin tags must be an array"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
errors
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Validate version compatibility
|
|
209
|
+
#
|
|
210
|
+
# Checks if the plugin's required Cabriolet version matches the
|
|
211
|
+
# current version.
|
|
212
|
+
#
|
|
213
|
+
# @param plugin_version [String] Required Cabriolet version
|
|
214
|
+
# @param cabriolet_version [String] Current Cabriolet version
|
|
215
|
+
#
|
|
216
|
+
# @return [Array<String>] List of version errors (empty if
|
|
217
|
+
# compatible)
|
|
218
|
+
#
|
|
219
|
+
# @example Compatible version
|
|
220
|
+
# PluginValidator.validate_version_compatibility("~> 0.1", "0.1.0")
|
|
221
|
+
# #=> []
|
|
222
|
+
#
|
|
223
|
+
# @example Incompatible version
|
|
224
|
+
# PluginValidator.validate_version_compatibility(">= 2.0", "0.1.0")
|
|
225
|
+
# #=> ["Plugin requires Cabriolet version >= 2.0, ..."]
|
|
226
|
+
def validate_version_compatibility(plugin_version, cabriolet_version)
|
|
227
|
+
errors = []
|
|
228
|
+
|
|
229
|
+
# Parse version requirement
|
|
230
|
+
if plugin_version.start_with?("~>")
|
|
231
|
+
# Pessimistic version constraint
|
|
232
|
+
required = plugin_version.sub("~>", "").strip
|
|
233
|
+
unless version_compatible?(cabriolet_version, required, :pessimistic)
|
|
234
|
+
errors << "Plugin requires Cabriolet version ~> #{required}, " \
|
|
235
|
+
"but #{cabriolet_version} is installed"
|
|
236
|
+
end
|
|
237
|
+
elsif plugin_version.start_with?(">=")
|
|
238
|
+
# Minimum version
|
|
239
|
+
required = plugin_version.sub(">=", "").strip
|
|
240
|
+
unless version_compatible?(cabriolet_version, required, :gte)
|
|
241
|
+
errors << "Plugin requires Cabriolet version >= #{required}, " \
|
|
242
|
+
"but #{cabriolet_version} is installed"
|
|
243
|
+
end
|
|
244
|
+
elsif plugin_version.start_with?("=")
|
|
245
|
+
# Exact version
|
|
246
|
+
required = plugin_version.sub("=", "").strip
|
|
247
|
+
unless cabriolet_version == required
|
|
248
|
+
errors << "Plugin requires exact Cabriolet version #{required}, " \
|
|
249
|
+
"but #{cabriolet_version} is installed"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
errors
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Validate plugin dependencies
|
|
257
|
+
#
|
|
258
|
+
# Checks if dependency specifications are valid. This performs
|
|
259
|
+
# format validation only; actual dependency resolution happens at
|
|
260
|
+
# load time.
|
|
261
|
+
#
|
|
262
|
+
# @param dependencies [Array<String>] Dependency specifications
|
|
263
|
+
#
|
|
264
|
+
# @return [Array<String>] List of validation warnings
|
|
265
|
+
#
|
|
266
|
+
# @example Valid dependencies
|
|
267
|
+
# deps = ["other-plugin >= 1.0"]
|
|
268
|
+
# PluginValidator.validate_dependencies(deps)
|
|
269
|
+
# #=> []
|
|
270
|
+
def validate_dependencies(dependencies)
|
|
271
|
+
warnings = []
|
|
272
|
+
|
|
273
|
+
unless dependencies.is_a?(Array)
|
|
274
|
+
warnings << "Dependencies must be an array"
|
|
275
|
+
return warnings
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
dependencies.each do |dep|
|
|
279
|
+
unless dep.is_a?(String)
|
|
280
|
+
warnings << "Each dependency must be a string"
|
|
281
|
+
next
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
parts = dep.split
|
|
285
|
+
if parts.empty?
|
|
286
|
+
warnings << "Empty dependency specification"
|
|
287
|
+
elsif !/^[a-z0-9_-]+$/.match?(parts[0])
|
|
288
|
+
warnings << "Invalid dependency name: #{parts[0]}"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
warnings
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Check plugin for potentially dangerous code
|
|
296
|
+
#
|
|
297
|
+
# Scans the plugin's source code for dangerous method calls that
|
|
298
|
+
# might pose security risks.
|
|
299
|
+
#
|
|
300
|
+
# @param plugin_class [Class] Plugin class to check
|
|
301
|
+
#
|
|
302
|
+
# @return [Array<String>] List of safety warnings
|
|
303
|
+
#
|
|
304
|
+
# @example Safe plugin
|
|
305
|
+
# PluginValidator.check_safety(MySafePlugin)
|
|
306
|
+
# #=> []
|
|
307
|
+
#
|
|
308
|
+
# @example Potentially dangerous plugin
|
|
309
|
+
# PluginValidator.check_safety(MyDangerousPlugin)
|
|
310
|
+
# #=> ["Uses system call in setup method"]
|
|
311
|
+
def check_safety(plugin_class)
|
|
312
|
+
warnings = []
|
|
313
|
+
|
|
314
|
+
# Get source location
|
|
315
|
+
begin
|
|
316
|
+
methods_to_check = %i[setup activate metadata]
|
|
317
|
+
|
|
318
|
+
methods_to_check.each do |method_name|
|
|
319
|
+
next unless plugin_class.method_defined?(method_name, false)
|
|
320
|
+
|
|
321
|
+
method_obj = plugin_class.instance_method(method_name)
|
|
322
|
+
source_location = method_obj.source_location
|
|
323
|
+
|
|
324
|
+
if source_location && File.exist?(source_location[0])
|
|
325
|
+
source = File.read(source_location[0])
|
|
326
|
+
|
|
327
|
+
DANGEROUS_METHODS.each do |dangerous|
|
|
328
|
+
pattern = /\b#{Regexp.escape(dangerous)}\b/
|
|
329
|
+
if source&.match?(pattern)
|
|
330
|
+
warnings << "Plugin uses potentially dangerous method " \
|
|
331
|
+
"'#{dangerous}' " \
|
|
332
|
+
"in #{source_location[0]}"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
rescue StandardError => e
|
|
338
|
+
warnings << "Could not perform safety check: #{e.message}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
warnings
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private
|
|
345
|
+
|
|
346
|
+
# Check if a version string is valid
|
|
347
|
+
#
|
|
348
|
+
# @param version [String] Version string to check
|
|
349
|
+
#
|
|
350
|
+
# @return [Boolean] True if valid
|
|
351
|
+
def valid_version?(version)
|
|
352
|
+
version.is_a?(String) && version =~ /^\d+\.\d+(\.\d+)?$/
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Check if a URL is valid
|
|
356
|
+
#
|
|
357
|
+
# @param url [String] URL string to check
|
|
358
|
+
#
|
|
359
|
+
# @return [Boolean] True if valid
|
|
360
|
+
def valid_url?(url)
|
|
361
|
+
url.is_a?(String) && url =~ %r{^https?://}
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Check version compatibility
|
|
365
|
+
#
|
|
366
|
+
# @param actual [String] Actual version
|
|
367
|
+
# @param required [String] Required version
|
|
368
|
+
# @param constraint [Symbol] Constraint type (:gte, :pessimistic)
|
|
369
|
+
#
|
|
370
|
+
# @return [Boolean] True if compatible
|
|
371
|
+
def version_compatible?(actual, required, constraint)
|
|
372
|
+
actual_parts = actual.split(".").map(&:to_i)
|
|
373
|
+
required_parts = required.split(".").map(&:to_i)
|
|
374
|
+
|
|
375
|
+
case constraint
|
|
376
|
+
when :gte
|
|
377
|
+
compare_versions(actual_parts, required_parts) >= 0
|
|
378
|
+
when :pessimistic
|
|
379
|
+
# ~> 1.2 means >= 1.2 and < 2.0
|
|
380
|
+
# ~> 1.2.3 means >= 1.2.3 and < 1.3
|
|
381
|
+
return false if compare_versions(actual_parts,
|
|
382
|
+
required_parts).negative?
|
|
383
|
+
|
|
384
|
+
upper = required_parts.dup
|
|
385
|
+
if required_parts.length >= 3
|
|
386
|
+
# Patch-level constraint
|
|
387
|
+
upper[1] += 1
|
|
388
|
+
upper[2] = 0
|
|
389
|
+
else
|
|
390
|
+
# Minor-level constraint
|
|
391
|
+
upper[0] += 1
|
|
392
|
+
upper[1] = 0
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
compare_versions(actual_parts, upper).negative?
|
|
396
|
+
else
|
|
397
|
+
false
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Compare version part arrays
|
|
402
|
+
#
|
|
403
|
+
# @param v1 [Array<Integer>] Version 1 parts
|
|
404
|
+
# @param v2 [Array<Integer>] Version 2 parts
|
|
405
|
+
#
|
|
406
|
+
# @return [Integer] -1, 0, or 1
|
|
407
|
+
def compare_versions(v1, v2)
|
|
408
|
+
max_length = [v1.length, v2.length].max
|
|
409
|
+
|
|
410
|
+
max_length.times do |i|
|
|
411
|
+
p1 = v1[i] || 0
|
|
412
|
+
p2 = v2[i] || 0
|
|
413
|
+
|
|
414
|
+
return -1 if p1 < p2
|
|
415
|
+
return 1 if p1 > p2
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
0
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
@@ -19,13 +19,19 @@ module Cabriolet
|
|
|
19
19
|
|
|
20
20
|
# Read bytes from memory
|
|
21
21
|
#
|
|
22
|
-
# @param bytes [Integer] Number of bytes to read
|
|
22
|
+
# @param bytes [Integer, nil] Number of bytes to read (nil = read all remaining)
|
|
23
23
|
# @return [String] Bytes read (binary encoding)
|
|
24
|
-
def read(bytes)
|
|
24
|
+
def read(bytes = nil)
|
|
25
25
|
return "" if @pos >= @data.bytesize
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if bytes.nil?
|
|
28
|
+
# Read all remaining data
|
|
29
|
+
result = @data.byteslice(@pos..-1) || ""
|
|
30
|
+
@pos = @data.bytesize
|
|
31
|
+
else
|
|
32
|
+
result = @data.byteslice(@pos, bytes) || ""
|
|
33
|
+
@pos += result.bytesize
|
|
34
|
+
end
|
|
29
35
|
result
|
|
30
36
|
end
|
|
31
37
|
|
|
@@ -77,6 +83,13 @@ module Cabriolet
|
|
|
77
83
|
@pos
|
|
78
84
|
end
|
|
79
85
|
|
|
86
|
+
# Rewind to the beginning of the handle
|
|
87
|
+
#
|
|
88
|
+
# @return [Integer] New position (0)
|
|
89
|
+
def rewind
|
|
90
|
+
@pos = 0
|
|
91
|
+
end
|
|
92
|
+
|
|
80
93
|
# Close the handle
|
|
81
94
|
#
|
|
82
95
|
# @return [void]
|
|
@@ -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
|