cabriolet 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +703 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +167 -16
- data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +108 -84
- data/lib/cabriolet/cab/decompressor.rb +16 -20
- data/lib/cabriolet/cab/extractor.rb +142 -66
- data/lib/cabriolet/cab/file_compression_work.rb +52 -0
- data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
- data/lib/cabriolet/checksum.rb +49 -0
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/collections/file_collection.rb +175 -0
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +36 -95
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +83 -53
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/extraction/base_extractor.rb +88 -0
- data/lib/cabriolet/extraction/extractor.rb +171 -0
- data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
- data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/format_base.rb +79 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
- data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
- data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/encoder.rb +15 -12
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +119 -168
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/directory_builder.rb +153 -0
- data/lib/cabriolet/lit/guid_generator.rb +16 -0
- data/lib/cabriolet/lit/header_writer.rb +124 -0
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/quantum_shared.rb +105 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +181 -20
- metadata +69 -4
- data/lib/cabriolet/auto.rb +0 -173
- data/lib/cabriolet/parallel.rb +0 -333
data/lib/cabriolet/cli.rb
CHANGED
|
@@ -2,88 +2,130 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
|
|
5
|
+
require_relative "cli/command_registry"
|
|
6
|
+
require_relative "cli/command_dispatcher"
|
|
7
|
+
|
|
8
|
+
# Register all format handlers with the command registry
|
|
9
|
+
require_relative "cab/command_handler"
|
|
10
|
+
require_relative "chm/command_handler"
|
|
11
|
+
require_relative "szdd/command_handler"
|
|
12
|
+
require_relative "kwaj/command_handler"
|
|
13
|
+
require_relative "hlp/command_handler"
|
|
14
|
+
require_relative "lit/command_handler"
|
|
15
|
+
require_relative "oab/command_handler"
|
|
16
|
+
|
|
17
|
+
Cabriolet::Commands::CommandRegistry.register_format(:cab, Cabriolet::CAB::CommandHandler)
|
|
18
|
+
Cabriolet::Commands::CommandRegistry.register_format(:chm, Cabriolet::CHM::CommandHandler)
|
|
19
|
+
Cabriolet::Commands::CommandRegistry.register_format(:szdd, Cabriolet::SZDD::CommandHandler)
|
|
20
|
+
Cabriolet::Commands::CommandRegistry.register_format(:kwaj, Cabriolet::KWAJ::CommandHandler)
|
|
21
|
+
Cabriolet::Commands::CommandRegistry.register_format(:hlp, Cabriolet::HLP::CommandHandler)
|
|
22
|
+
Cabriolet::Commands::CommandRegistry.register_format(:lit, Cabriolet::LIT::CommandHandler)
|
|
23
|
+
Cabriolet::Commands::CommandRegistry.register_format(:oab, Cabriolet::OAB::CommandHandler)
|
|
24
|
+
|
|
5
25
|
module Cabriolet
|
|
6
|
-
# CLI provides command-line interface for Cabriolet
|
|
26
|
+
# CLI provides unified command-line interface for Cabriolet
|
|
27
|
+
#
|
|
28
|
+
# The CLI uses auto-detection to determine the format of input files,
|
|
29
|
+
# then dispatches commands to the appropriate format handler.
|
|
30
|
+
# A --format option allows manual override when needed.
|
|
31
|
+
#
|
|
32
|
+
# Legacy format-specific commands are maintained for backward compatibility.
|
|
33
|
+
#
|
|
7
34
|
class CLI < Thor
|
|
8
35
|
def self.exit_on_failure?
|
|
9
36
|
true
|
|
10
37
|
end
|
|
11
38
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def list(file)
|
|
16
|
-
setup_verbose(options[:verbose])
|
|
39
|
+
# Global option for format override
|
|
40
|
+
class_option :format, type: :string, enum: %w[cab chm szdd kwaj hlp lit oab],
|
|
41
|
+
desc: "Force format (overrides auto-detection)"
|
|
17
42
|
|
|
18
|
-
|
|
19
|
-
|
|
43
|
+
# Global option for verbose output
|
|
44
|
+
class_option :verbose, type: :boolean, aliases: "-v",
|
|
45
|
+
desc: "Enable verbose output"
|
|
20
46
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
puts "\nFiles:"
|
|
47
|
+
# ==========================================================================
|
|
48
|
+
# Unified Commands (auto-detect format)
|
|
49
|
+
# ==========================================================================
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
abort "Error: #{e.message}"
|
|
51
|
+
desc "list FILE", "List contents of archive file (auto-detects format)"
|
|
52
|
+
option :format, type: :string, hide: true # Deprecated, use global --format
|
|
53
|
+
def list(file)
|
|
54
|
+
run_dispatcher(:list, file, **options)
|
|
31
55
|
end
|
|
32
56
|
|
|
33
|
-
desc "extract FILE [OUTPUT_DIR]",
|
|
34
|
-
|
|
35
|
-
option :
|
|
36
|
-
|
|
57
|
+
desc "extract FILE [OUTPUT_DIR]",
|
|
58
|
+
"Extract files from archive (auto-detects format)"
|
|
59
|
+
option :output, type: :string, aliases: "-o",
|
|
60
|
+
desc: "Output file/directory path"
|
|
37
61
|
option :salvage, type: :boolean,
|
|
38
|
-
desc: "Enable salvage mode for corrupted files"
|
|
62
|
+
desc: "Enable salvage mode for corrupted files (CAB only)"
|
|
63
|
+
option :base_file, type: :string,
|
|
64
|
+
desc: "Base file for incremental patches (OAB only)"
|
|
65
|
+
option :use_manifest, type: :boolean,
|
|
66
|
+
desc: "Use manifest for filenames (LIT only)"
|
|
67
|
+
option :format, type: :string, hide: true # Deprecated, use global --format
|
|
39
68
|
def extract(file, output_dir = nil)
|
|
40
|
-
|
|
41
|
-
|
|
69
|
+
run_dispatcher(:extract, file, output_dir, **options)
|
|
70
|
+
end
|
|
42
71
|
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
desc "create OUTPUT FILES...",
|
|
73
|
+
"Create archive file (auto-detects format from extension)"
|
|
74
|
+
option :compression, type: :string, enum: %w[none mszip lzx quantum],
|
|
75
|
+
desc: "Compression type (CAB only)"
|
|
76
|
+
option :format, type: :string, enum: %w[cab chm szdd kwaj hlp lit oab],
|
|
77
|
+
desc: "Output format (default: auto-detect from OUTPUT extension)"
|
|
78
|
+
option :window_bits, type: :numeric, desc: "LZX window size for CHM (15-21)"
|
|
79
|
+
option :missing_char, type: :string, desc: "Missing character for SZDD"
|
|
80
|
+
option :szdd_format, type: :string, enum: %w[normal qbasic],
|
|
81
|
+
desc: "SZDD format variant (default: normal)"
|
|
82
|
+
option :kwaj_compression, type: :string, enum: %w[none xor szdd mszip],
|
|
83
|
+
desc: "KWAJ compression method (default: szdd)"
|
|
84
|
+
option :include_length, type: :boolean,
|
|
85
|
+
desc: "Include length in KWAJ header"
|
|
86
|
+
option :kwaj_filename, type: :string,
|
|
87
|
+
desc: "Original filename for KWAJ header"
|
|
88
|
+
option :extra_data, type: :string, desc: "Extra data for KWAJ header"
|
|
89
|
+
option :hlp_format, type: :string, enum: %w[quickhelp winhelp],
|
|
90
|
+
desc: "HLP format variant (default: quickhelp)"
|
|
91
|
+
option :language_id, type: :string,
|
|
92
|
+
desc: "Language ID for LIT (e.g., 0x409)"
|
|
93
|
+
option :lit_version, type: :numeric, desc: "LIT format version (default: 1)"
|
|
94
|
+
option :block_size, type: :numeric, desc: "Block size for OAB compression"
|
|
95
|
+
option :compress, type: :boolean, default: true,
|
|
96
|
+
desc: "Compress files in HLP/LIT"
|
|
97
|
+
def create(output, *files)
|
|
98
|
+
# Normalize options for create command
|
|
99
|
+
create_options = normalize_create_options(options)
|
|
45
100
|
|
|
46
|
-
|
|
47
|
-
|
|
101
|
+
# Detect format from output extension if not specified
|
|
102
|
+
format = detect_format_from_output(output, create_options[:format])
|
|
48
103
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
104
|
+
# Set the format in options for the dispatcher
|
|
105
|
+
create_options[:format] = format
|
|
106
|
+
|
|
107
|
+
run_dispatcher(:create, output, files, **create_options)
|
|
52
108
|
end
|
|
53
109
|
|
|
54
|
-
desc "info FILE",
|
|
55
|
-
|
|
56
|
-
|
|
110
|
+
desc "info FILE",
|
|
111
|
+
"Show detailed archive file information (auto-detects format)"
|
|
112
|
+
option :format, type: :string, hide: true # Deprecated, use global --format
|
|
57
113
|
def info(file)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
decompressor = CAB::Decompressor.new
|
|
61
|
-
cabinet = decompressor.open(file)
|
|
62
|
-
|
|
63
|
-
display_cabinet_info(cabinet)
|
|
64
|
-
rescue Error => e
|
|
65
|
-
abort "Error: #{e.message}"
|
|
114
|
+
run_dispatcher(:info, file, **options)
|
|
66
115
|
end
|
|
67
116
|
|
|
68
|
-
desc "test FILE", "Test
|
|
69
|
-
option :
|
|
70
|
-
desc: "Enable verbose output"
|
|
117
|
+
desc "test FILE", "Test archive file integrity (auto-detects format)"
|
|
118
|
+
option :format, type: :string, hide: true # Deprecated, use global --format
|
|
71
119
|
def test(file)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
decompressor = CAB::Decompressor.new
|
|
75
|
-
cabinet = decompressor.open(file)
|
|
76
|
-
|
|
77
|
-
puts "Testing #{cabinet.filename}..."
|
|
78
|
-
# TODO: Implement integrity testing
|
|
79
|
-
puts "OK: All #{cabinet.file_count} files passed integrity check"
|
|
80
|
-
rescue Error => e
|
|
81
|
-
abort "Error: #{e.message}"
|
|
120
|
+
run_dispatcher(:test, file, **options)
|
|
82
121
|
end
|
|
83
122
|
|
|
123
|
+
# ==========================================================================
|
|
124
|
+
# Legacy Commands (maintained for backward compatibility)
|
|
125
|
+
# ==========================================================================
|
|
126
|
+
|
|
127
|
+
# CAB-specific legacy commands
|
|
84
128
|
desc "search FILE", "Search for embedded CAB files"
|
|
85
|
-
option :verbose, type: :boolean, aliases: "-v",
|
|
86
|
-
desc: "Enable verbose output"
|
|
87
129
|
def search(file)
|
|
88
130
|
setup_verbose(options[:verbose])
|
|
89
131
|
|
|
@@ -107,579 +149,265 @@ module Cabriolet
|
|
|
107
149
|
abort "Error: #{e.message}"
|
|
108
150
|
end
|
|
109
151
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
option :verbose, type: :boolean, aliases: "-v",
|
|
114
|
-
desc: "Enable verbose output"
|
|
115
|
-
def create(output, *files)
|
|
116
|
-
setup_verbose(options[:verbose])
|
|
117
|
-
|
|
118
|
-
raise ArgumentError, "No files specified" if files.empty?
|
|
119
|
-
|
|
120
|
-
files.each do |f|
|
|
121
|
-
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
compressor = CAB::Compressor.new
|
|
125
|
-
files.each { |f| compressor.add_file(f) }
|
|
126
|
-
|
|
127
|
-
puts "Creating #{output} with #{files.size} file(s) (#{options[:compression]} compression)" if options[:verbose]
|
|
128
|
-
bytes = compressor.generate(output,
|
|
129
|
-
compression: options[:compression].to_sym)
|
|
130
|
-
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
131
|
-
rescue Error => e
|
|
132
|
-
abort "Error: #{e.message}"
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# CHM commands
|
|
136
|
-
desc "chm-list FILE", "List contents of CHM file"
|
|
152
|
+
# CHM legacy commands (aliases to unified commands with format override)
|
|
153
|
+
desc "chm-list FILE",
|
|
154
|
+
"List contents of CHM file (legacy, use: list --format chm FILE)"
|
|
137
155
|
option :verbose, type: :boolean, aliases: "-v",
|
|
138
156
|
desc: "Enable verbose output"
|
|
139
157
|
def chm_list(file)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
decompressor = CHM::Decompressor.new
|
|
143
|
-
chm = decompressor.open(file)
|
|
144
|
-
|
|
145
|
-
puts "CHM File: #{chm.filename}"
|
|
146
|
-
puts "Version: #{chm.version}"
|
|
147
|
-
puts "Language: #{chm.language}"
|
|
148
|
-
puts "Chunks: #{chm.num_chunks}, Chunk Size: #{chm.chunk_size}"
|
|
149
|
-
puts "\nFiles:"
|
|
150
|
-
|
|
151
|
-
chm.all_files.each do |f|
|
|
152
|
-
section_name = f.section.id.zero? ? "Uncompressed" : "MSCompressed"
|
|
153
|
-
puts " #{f.filename} (#{f.length} bytes, #{section_name})"
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
decompressor.close
|
|
157
|
-
rescue Error => e
|
|
158
|
-
abort "Error: #{e.message}"
|
|
158
|
+
run_with_format(:list, :chm, file, verbose: options[:verbose])
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
-
desc "chm-extract FILE [OUTPUT_DIR]",
|
|
161
|
+
desc "chm-extract FILE [OUTPUT_DIR]",
|
|
162
|
+
"Extract files from CHM (legacy, use: extract --format chm FILE)"
|
|
162
163
|
option :output, type: :string, aliases: "-o", desc: "Output directory"
|
|
163
164
|
option :verbose, type: :boolean, aliases: "-v",
|
|
164
165
|
desc: "Enable verbose output"
|
|
165
166
|
def chm_extract(file, output_dir = nil)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
decompressor = CHM::Decompressor.new
|
|
170
|
-
chm = decompressor.open(file)
|
|
171
|
-
|
|
172
|
-
require "fileutils"
|
|
173
|
-
FileUtils.mkdir_p(output_dir)
|
|
174
|
-
|
|
175
|
-
count = 0
|
|
176
|
-
chm.all_files.each do |f|
|
|
177
|
-
next if f.system_file?
|
|
178
|
-
|
|
179
|
-
output_path = File.join(output_dir, f.filename)
|
|
180
|
-
output_subdir = File.dirname(output_path)
|
|
181
|
-
FileUtils.mkdir_p(output_subdir)
|
|
182
|
-
|
|
183
|
-
puts "Extracting: #{f.filename}" if options[:verbose]
|
|
184
|
-
decompressor.extract(f, output_path)
|
|
185
|
-
count += 1
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
decompressor.close
|
|
189
|
-
puts "Extracted #{count} file(s) to #{output_dir}"
|
|
190
|
-
rescue Error => e
|
|
191
|
-
abort "Error: #{e.message}"
|
|
167
|
+
opts = { verbose: options[:verbose], output: options[:output] }
|
|
168
|
+
run_with_format(:extract, :chm, file, output_dir, **opts)
|
|
192
169
|
end
|
|
193
170
|
|
|
194
|
-
desc "chm-info FILE",
|
|
171
|
+
desc "chm-info FILE",
|
|
172
|
+
"Show CHM file information (legacy, use: info --format chm FILE)"
|
|
195
173
|
option :verbose, type: :boolean, aliases: "-v",
|
|
196
174
|
desc: "Enable verbose output"
|
|
197
175
|
def chm_info(file)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
decompressor = CHM::Decompressor.new
|
|
201
|
-
chm = decompressor.open(file)
|
|
202
|
-
|
|
203
|
-
display_chm_info(chm)
|
|
204
|
-
decompressor.close
|
|
205
|
-
rescue Error => e
|
|
206
|
-
abort "Error: #{e.message}"
|
|
207
|
-
|
|
208
|
-
desc "chm-create OUTPUT FILES...", "Create a CHM file from HTML files"
|
|
209
|
-
option :window_bits, type: :numeric, default: 16,
|
|
210
|
-
desc: "LZX window size (15-21)"
|
|
211
|
-
option :verbose, type: :boolean, aliases: "-v",
|
|
212
|
-
desc: "Enable verbose output"
|
|
213
|
-
def chm_create(output, *files)
|
|
214
|
-
setup_verbose(options[:verbose])
|
|
215
|
-
|
|
216
|
-
raise ArgumentError, "No files specified" if files.empty?
|
|
217
|
-
|
|
218
|
-
files.each do |f|
|
|
219
|
-
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
compressor = CHM::Compressor.new
|
|
223
|
-
files.each do |f|
|
|
224
|
-
# Default to compressed section for .html, uncompressed for images
|
|
225
|
-
section = f.end_with?(".html", ".htm") ? :compressed : :uncompressed
|
|
226
|
-
compressor.add_file(f, "/#{File.basename(f)}", section: section)
|
|
227
|
-
end
|
|
176
|
+
run_with_format(:info, :chm, file, verbose: options[:verbose])
|
|
177
|
+
end
|
|
228
178
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
179
|
+
desc "chm-create OUTPUT FILES...",
|
|
180
|
+
"Create CHM file (legacy, use: create --format chm OUTPUT FILES...)"
|
|
181
|
+
option :window_bits, type: :numeric, default: 16,
|
|
182
|
+
desc: "LZX window size (15-21)"
|
|
183
|
+
option :verbose, type: :boolean, aliases: "-v",
|
|
184
|
+
desc: "Enable verbose output"
|
|
185
|
+
def chm_create(output, *files)
|
|
186
|
+
opts = { verbose: options[:verbose], window_bits: options[:window_bits],
|
|
187
|
+
format: :chm }
|
|
188
|
+
run_dispatcher(:create, output, files, **opts)
|
|
237
189
|
end
|
|
238
190
|
|
|
239
|
-
# SZDD commands
|
|
191
|
+
# SZDD legacy commands
|
|
240
192
|
desc "expand FILE [OUTPUT]",
|
|
241
|
-
"Expand SZDD
|
|
193
|
+
"Expand SZDD file (legacy, use: extract --format szdd FILE OUTPUT)"
|
|
242
194
|
option :output, type: :string, aliases: "-o", desc: "Output file path"
|
|
243
195
|
option :verbose, type: :boolean, aliases: "-v",
|
|
244
196
|
desc: "Enable verbose output"
|
|
245
197
|
def expand(file, output = nil)
|
|
246
|
-
|
|
247
|
-
output
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
header = decompressor.open(file)
|
|
251
|
-
|
|
252
|
-
# Auto-detect output name if not provided
|
|
253
|
-
output ||= decompressor.auto_output_filename(file, header)
|
|
254
|
-
|
|
255
|
-
puts "Expanding #{file} -> #{output}" if options[:verbose]
|
|
256
|
-
bytes = decompressor.extract(header, output)
|
|
257
|
-
decompressor.close(header)
|
|
258
|
-
|
|
259
|
-
puts "Expanded #{file} to #{output} (#{bytes} bytes)"
|
|
260
|
-
rescue Error => e
|
|
261
|
-
abort "Error: #{e.message}"
|
|
198
|
+
# Use positional output if provided, otherwise use option
|
|
199
|
+
final_output = output || options[:output]
|
|
200
|
+
opts = { verbose: options[:verbose], output: final_output, format: :szdd }
|
|
201
|
+
run_dispatcher(:extract, file, final_output, **opts)
|
|
262
202
|
end
|
|
263
203
|
|
|
264
|
-
desc "szdd-info FILE",
|
|
204
|
+
desc "szdd-info FILE",
|
|
205
|
+
"Show SZDD file information (legacy, use: info --format szdd FILE)"
|
|
265
206
|
option :verbose, type: :boolean, aliases: "-v",
|
|
266
207
|
desc: "Enable verbose output"
|
|
267
208
|
def szdd_info(file)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
decompressor = SZDD::Decompressor.new
|
|
271
|
-
header = decompressor.open(file)
|
|
272
|
-
|
|
273
|
-
puts "SZDD File Information"
|
|
274
|
-
puts "=" * 50
|
|
275
|
-
puts "Filename: #{file}"
|
|
276
|
-
puts "Format: #{header.format.to_s.upcase}"
|
|
277
|
-
puts "Uncompressed size: #{header.length} bytes"
|
|
278
|
-
if header.missing_char
|
|
279
|
-
puts "Missing character: '#{header.missing_char}'"
|
|
280
|
-
puts "Suggested filename: #{header.suggested_filename(File.basename(file))}"
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
decompressor.close(header)
|
|
284
|
-
rescue Error => e
|
|
285
|
-
abort "Error: #{e.message}"
|
|
209
|
+
run_with_format(:info, :szdd, file, verbose: options[:verbose])
|
|
286
210
|
end
|
|
287
211
|
|
|
288
212
|
desc "compress FILE [OUTPUT]",
|
|
289
|
-
"Compress
|
|
213
|
+
"Compress to SZDD format (legacy, use: create --format szdd OUTPUT FILE)"
|
|
290
214
|
option :output, type: :string, aliases: "-o", desc: "Output file path"
|
|
291
|
-
option :missing_char, type: :string,
|
|
292
|
-
desc: "Missing character for filename reconstruction"
|
|
215
|
+
option :missing_char, type: :string, desc: "Missing character for filename"
|
|
293
216
|
option :format, type: :string, enum: %w[normal qbasic], default: "normal",
|
|
294
217
|
desc: "SZDD format (normal or qbasic)"
|
|
295
218
|
option :verbose, type: :boolean, aliases: "-v",
|
|
296
219
|
desc: "Enable verbose output"
|
|
297
220
|
def compress(file, output = nil)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
221
|
+
# SZDD format option refers to SZDD variant (normal/qbasic), not file format
|
|
222
|
+
# File format is always :szdd, variant is passed as szdd_format
|
|
223
|
+
szdd_variant = options[:format] || "normal"
|
|
224
|
+
|
|
225
|
+
opts = {
|
|
226
|
+
verbose: options[:verbose],
|
|
227
|
+
output: options[:output],
|
|
228
|
+
format: :szdd, # Always SZDD format
|
|
229
|
+
szdd_format: szdd_variant.to_sym, # Pass variant as szdd_format
|
|
230
|
+
missing_char: options[:missing_char],
|
|
231
|
+
}
|
|
232
|
+
# SZDD convention: auto-generate output name
|
|
233
|
+
output ||= opts[:output]
|
|
302
234
|
if output.nil?
|
|
303
|
-
|
|
304
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if options[:missing_char]
|
|
314
|
-
compress_options[:missing_char] =
|
|
315
|
-
options[:missing_char]
|
|
235
|
+
ext = File.extname(file)
|
|
236
|
+
# SZDD format: last character of extension replaced with underscore
|
|
237
|
+
# e.g., file.txt -> file.tx_, file.c -> file.c_
|
|
238
|
+
if ext.length.between?(2, 4) # .c, .txt, .html, etc.
|
|
239
|
+
base = File.basename(file, ext)
|
|
240
|
+
output = "#{base}#{ext.chomp(ext[-1])}_"
|
|
241
|
+
else
|
|
242
|
+
output = "#{file}_"
|
|
243
|
+
end
|
|
244
|
+
opts[:output] = output
|
|
316
245
|
end
|
|
317
|
-
|
|
318
|
-
bytes = compressor.compress(file, output, **compress_options)
|
|
319
|
-
|
|
320
|
-
puts "Compressed #{file} to #{output} (#{bytes} bytes)"
|
|
321
|
-
rescue Error => e
|
|
322
|
-
abort "Error: #{e.message}"
|
|
246
|
+
run_dispatcher(:create, output, [file], **opts)
|
|
323
247
|
end
|
|
324
248
|
|
|
325
|
-
# KWAJ commands
|
|
326
|
-
desc "kwaj-extract FILE [OUTPUT]",
|
|
249
|
+
# KWAJ legacy commands
|
|
250
|
+
desc "kwaj-extract FILE [OUTPUT]",
|
|
251
|
+
"Extract KWAJ file (legacy, use: extract --format kwaj FILE OUTPUT)"
|
|
327
252
|
option :output, type: :string, aliases: "-o", desc: "Output file path"
|
|
328
253
|
option :verbose, type: :boolean, aliases: "-v",
|
|
329
254
|
desc: "Enable verbose output"
|
|
330
255
|
def kwaj_extract(file, output = nil)
|
|
331
|
-
|
|
332
|
-
output
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
header = decompressor.open(file)
|
|
336
|
-
|
|
337
|
-
# Auto-detect output name if not provided
|
|
338
|
-
output ||= decompressor.auto_output_filename(file, header)
|
|
339
|
-
|
|
340
|
-
puts "Extracting #{file} -> #{output}" if options[:verbose]
|
|
341
|
-
bytes = decompressor.extract(header, file, output)
|
|
342
|
-
decompressor.close(header)
|
|
343
|
-
|
|
344
|
-
puts "Extracted #{file} to #{output} (#{bytes} bytes)"
|
|
345
|
-
rescue Error => e
|
|
346
|
-
abort "Error: #{e.message}"
|
|
256
|
+
# Use positional output if provided, otherwise use option
|
|
257
|
+
output_file = output || options[:output]
|
|
258
|
+
opts = { verbose: options[:verbose], output: output_file, format: :kwaj }
|
|
259
|
+
run_dispatcher(:extract, file, nil, **opts)
|
|
347
260
|
end
|
|
348
261
|
|
|
349
|
-
desc "kwaj-info FILE",
|
|
262
|
+
desc "kwaj-info FILE",
|
|
263
|
+
"Show KWAJ file information (legacy, use: info --format kwaj FILE)"
|
|
350
264
|
option :verbose, type: :boolean, aliases: "-v",
|
|
351
265
|
desc: "Enable verbose output"
|
|
352
266
|
def kwaj_info(file)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
decompressor = KWAJ::Decompressor.new
|
|
356
|
-
header = decompressor.open(file)
|
|
357
|
-
|
|
358
|
-
puts "KWAJ File Information"
|
|
359
|
-
puts "=" * 50
|
|
360
|
-
puts "Filename: #{file}"
|
|
361
|
-
puts "Compression: #{header.compression_name}"
|
|
362
|
-
puts "Data offset: #{header.data_offset} bytes"
|
|
363
|
-
puts "Uncompressed size: #{header.length || 'unknown'} bytes"
|
|
364
|
-
puts "Original filename: #{header.filename}" if header.filename
|
|
365
|
-
if header.extra && !header.extra.empty?
|
|
366
|
-
puts "Extra data: #{header.extra_length} bytes"
|
|
367
|
-
puts " #{header.extra}"
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
decompressor.close(header)
|
|
371
|
-
rescue Error => e
|
|
372
|
-
abort "Error: #{e.message}"
|
|
267
|
+
run_with_format(:info, :kwaj, file, verbose: options[:verbose])
|
|
373
268
|
end
|
|
374
269
|
|
|
375
|
-
desc "kwaj-compress FILE [OUTPUT]",
|
|
270
|
+
desc "kwaj-compress FILE [OUTPUT]",
|
|
271
|
+
"Compress to KWAJ format (legacy, use: create --format kwaj OUTPUT FILE)"
|
|
376
272
|
option :output, type: :string, aliases: "-o", desc: "Output file path"
|
|
377
|
-
option :compression, type: :string, enum: %w[none xor szdd mszip],
|
|
378
|
-
|
|
379
|
-
option :include_length, type: :boolean,
|
|
380
|
-
|
|
381
|
-
option :
|
|
382
|
-
desc: "Original filename to embed in header"
|
|
383
|
-
option :extra_data, type: :string, desc: "Extra data to include in header"
|
|
273
|
+
option :compression, type: :string, enum: %w[none xor szdd mszip], default: "szdd",
|
|
274
|
+
desc: "Compression method"
|
|
275
|
+
option :include_length, type: :boolean, desc: "Include uncompressed length"
|
|
276
|
+
option :filename, type: :string, desc: "Original filename to embed"
|
|
277
|
+
option :extra_data, type: :string, desc: "Extra data to include"
|
|
384
278
|
option :verbose, type: :boolean, aliases: "-v",
|
|
385
279
|
desc: "Enable verbose output"
|
|
386
280
|
def kwaj_compress(file, output = nil)
|
|
387
|
-
|
|
388
|
-
output
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
compress_options[:filename] = options[:filename] if options[:filename]
|
|
400
|
-
if options[:extra_data]
|
|
401
|
-
compress_options[:extra_data] =
|
|
402
|
-
options[:extra_data]
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
bytes = compressor.compress(file, output, **compress_options)
|
|
406
|
-
|
|
407
|
-
puts "Compressed #{file} to #{output} (#{bytes} bytes, #{options[:compression]} compression)"
|
|
408
|
-
rescue Error => e
|
|
409
|
-
abort "Error: #{e.message}"
|
|
281
|
+
# Use positional output if provided, otherwise use option or default
|
|
282
|
+
output_file = output || options[:output] || "#{file}.kwj"
|
|
283
|
+
opts = {
|
|
284
|
+
verbose: options[:verbose],
|
|
285
|
+
output: output_file,
|
|
286
|
+
format: :kwaj,
|
|
287
|
+
compression: options[:compression],
|
|
288
|
+
include_length: options[:include_length],
|
|
289
|
+
filename: options[:filename],
|
|
290
|
+
extra_data: options[:extra_data],
|
|
291
|
+
}
|
|
292
|
+
run_dispatcher(:create, opts[:output], [file], **opts)
|
|
410
293
|
end
|
|
411
294
|
|
|
412
|
-
# HLP commands
|
|
413
|
-
desc "hlp-extract FILE [OUTPUT_DIR]",
|
|
295
|
+
# HLP legacy commands
|
|
296
|
+
desc "hlp-extract FILE [OUTPUT_DIR]",
|
|
297
|
+
"Extract HLP file (legacy, use: extract --format hlp FILE)"
|
|
414
298
|
option :output, type: :string, aliases: "-o", desc: "Output directory"
|
|
415
299
|
option :verbose, type: :boolean, aliases: "-v",
|
|
416
300
|
desc: "Enable verbose output"
|
|
417
301
|
def hlp_extract(file, output_dir = nil)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
decompressor = HLP::Decompressor.new
|
|
422
|
-
header = decompressor.open(file)
|
|
423
|
-
|
|
424
|
-
require "fileutils"
|
|
425
|
-
FileUtils.mkdir_p(output_dir)
|
|
426
|
-
|
|
427
|
-
puts "Extracting #{header.files.size} files from #{file}" if options[:verbose]
|
|
428
|
-
count = decompressor.extract_all(header, output_dir)
|
|
429
|
-
|
|
430
|
-
decompressor.close(header)
|
|
431
|
-
puts "Extracted #{count} file(s) to #{output_dir}"
|
|
432
|
-
rescue Error => e
|
|
433
|
-
abort "Error: #{e.message}"
|
|
302
|
+
opts = { verbose: options[:verbose], output: options[:output],
|
|
303
|
+
format: :hlp }
|
|
304
|
+
run_dispatcher(:extract, file, output_dir, **opts)
|
|
434
305
|
end
|
|
435
306
|
|
|
436
|
-
desc "hlp-
|
|
437
|
-
|
|
438
|
-
desc: "Compress files (LZSS MODE_MSHELP)"
|
|
307
|
+
desc "hlp-info FILE",
|
|
308
|
+
"Show HLP file information (legacy, use: info --format hlp FILE)"
|
|
439
309
|
option :verbose, type: :boolean, aliases: "-v",
|
|
440
310
|
desc: "Enable verbose output"
|
|
441
|
-
def
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
raise ArgumentError, "No files specified" if files.empty?
|
|
445
|
-
|
|
446
|
-
files.each do |f|
|
|
447
|
-
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
compressor = HLP::Compressor.new
|
|
451
|
-
files.each do |f|
|
|
452
|
-
compressor.add_file(f, File.basename(f), compress: options[:compress])
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
puts "Creating #{output} with #{files.size} file(s)" if options[:verbose]
|
|
456
|
-
bytes = compressor.generate(output)
|
|
457
|
-
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
458
|
-
rescue Error => e
|
|
459
|
-
abort "Error: #{e.message}"
|
|
311
|
+
def hlp_info(file)
|
|
312
|
+
run_with_format(:info, :hlp, file, verbose: options[:verbose])
|
|
460
313
|
end
|
|
461
314
|
|
|
462
|
-
desc "hlp-
|
|
315
|
+
desc "hlp-create OUTPUT FILES...",
|
|
316
|
+
"Create HLP file (legacy, use: create --format hlp OUTPUT FILES...)"
|
|
317
|
+
option :compress, type: :boolean, default: true, desc: "Compress files"
|
|
318
|
+
option :format, type: :string, enum: %w[quickhelp winhelp], default: "quickhelp",
|
|
319
|
+
desc: "HLP format variant"
|
|
463
320
|
option :verbose, type: :boolean, aliases: "-v",
|
|
464
321
|
desc: "Enable verbose output"
|
|
465
|
-
def
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
puts ""
|
|
477
|
-
puts "Files:"
|
|
478
|
-
header.files.each do |f|
|
|
479
|
-
compression = f.compressed? ? "LZSS" : "none"
|
|
480
|
-
puts " #{f.filename}"
|
|
481
|
-
puts " Uncompressed: #{f.length} bytes"
|
|
482
|
-
puts " Compressed: #{f.compressed_length} bytes (#{compression})"
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
decompressor.close(header)
|
|
486
|
-
rescue Error => e
|
|
487
|
-
abort "Error: #{e.message}"
|
|
322
|
+
def hlp_create(output, *files)
|
|
323
|
+
# Ensure format is set (default from Thor option or :hlp)
|
|
324
|
+
format = options[:format] || :hlp
|
|
325
|
+
|
|
326
|
+
opts = {
|
|
327
|
+
verbose: options[:verbose],
|
|
328
|
+
compress: options[:compress],
|
|
329
|
+
format: format,
|
|
330
|
+
hlp_format: options[:format],
|
|
331
|
+
}
|
|
332
|
+
run_dispatcher(:create, output, files, **opts)
|
|
488
333
|
end
|
|
489
334
|
|
|
490
|
-
# LIT commands
|
|
491
|
-
desc "lit-extract FILE [OUTPUT_DIR]",
|
|
335
|
+
# LIT legacy commands
|
|
336
|
+
desc "lit-extract FILE [OUTPUT_DIR]",
|
|
337
|
+
"Extract LIT file (legacy, use: extract --format lit FILE)"
|
|
492
338
|
option :output, type: :string, aliases: "-o", desc: "Output directory"
|
|
493
339
|
option :verbose, type: :boolean, aliases: "-v",
|
|
494
340
|
desc: "Enable verbose output"
|
|
495
341
|
def lit_extract(file, output_dir = nil)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
decompressor = LIT::Decompressor.new
|
|
500
|
-
header = decompressor.open(file)
|
|
501
|
-
|
|
502
|
-
abort "Error: LIT file is DRM-encrypted. Decryption not yet supported." if header.encrypted?
|
|
503
|
-
|
|
504
|
-
require "fileutils"
|
|
505
|
-
FileUtils.mkdir_p(output_dir)
|
|
506
|
-
|
|
507
|
-
puts "Extracting #{header.files.size} files from #{file}" if options[:verbose]
|
|
508
|
-
count = decompressor.extract_all(header, output_dir)
|
|
509
|
-
|
|
510
|
-
decompressor.close(header)
|
|
511
|
-
puts "Extracted #{count} file(s) to #{output_dir}"
|
|
512
|
-
rescue Error => e
|
|
513
|
-
abort "Error: #{e.message}"
|
|
514
|
-
rescue NotImplementedError => e
|
|
515
|
-
abort "Error: #{e.message}"
|
|
342
|
+
opts = { verbose: options[:verbose], output: options[:output],
|
|
343
|
+
format: :lit }
|
|
344
|
+
run_dispatcher(:extract, file, output_dir, **opts)
|
|
516
345
|
end
|
|
517
346
|
|
|
518
|
-
desc "lit-
|
|
519
|
-
|
|
520
|
-
desc: "Compress files with LZX"
|
|
347
|
+
desc "lit-info FILE",
|
|
348
|
+
"Show LIT file information (legacy, use: info --format lit FILE)"
|
|
521
349
|
option :verbose, type: :boolean, aliases: "-v",
|
|
522
350
|
desc: "Enable verbose output"
|
|
523
|
-
def
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
raise ArgumentError, "No files specified" if files.empty?
|
|
527
|
-
|
|
528
|
-
files.each do |f|
|
|
529
|
-
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
compressor = LIT::Compressor.new
|
|
533
|
-
files.each do |f|
|
|
534
|
-
compressor.add_file(f, File.basename(f), compress: options[:compress])
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
puts "Creating #{output} with #{files.size} file(s)" if options[:verbose]
|
|
538
|
-
bytes = compressor.generate(output)
|
|
539
|
-
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
540
|
-
rescue Error => e
|
|
541
|
-
abort "Error: #{e.message}"
|
|
542
|
-
rescue NotImplementedError => e
|
|
543
|
-
abort "Error: #{e.message}"
|
|
351
|
+
def lit_info(file)
|
|
352
|
+
run_with_format(:info, :lit, file, verbose: options[:verbose])
|
|
544
353
|
end
|
|
545
354
|
|
|
546
|
-
desc "lit-
|
|
355
|
+
desc "lit-create OUTPUT FILES...",
|
|
356
|
+
"Create LIT file (legacy, use: create --format lit OUTPUT FILES...)"
|
|
357
|
+
option :compress, type: :boolean, default: true,
|
|
358
|
+
desc: "Compress files with LZX"
|
|
359
|
+
option :language_id, type: :string,
|
|
360
|
+
desc: "Language ID (e.g., 0x409 for English)"
|
|
547
361
|
option :verbose, type: :boolean, aliases: "-v",
|
|
548
362
|
desc: "Enable verbose output"
|
|
549
|
-
def
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
puts "Filename: #{file}"
|
|
558
|
-
puts "Version: #{header.version}"
|
|
559
|
-
puts "Encrypted: #{header.encrypted? ? 'Yes (DES)' : 'No'}"
|
|
560
|
-
puts "Files: #{header.files.size}"
|
|
561
|
-
puts ""
|
|
562
|
-
puts "Files:"
|
|
563
|
-
header.files.each do |f|
|
|
564
|
-
compression = f.compressed? ? "LZX" : "none"
|
|
565
|
-
encryption = f.encrypted? ? " [encrypted]" : ""
|
|
566
|
-
puts " #{f.filename}"
|
|
567
|
-
puts " Size: #{f.length} bytes"
|
|
568
|
-
puts " Compression: #{compression}#{encryption}"
|
|
569
|
-
end
|
|
570
|
-
|
|
571
|
-
decompressor.close(header)
|
|
572
|
-
rescue Error => e
|
|
573
|
-
abort "Error: #{e.message}"
|
|
574
|
-
rescue NotImplementedError => e
|
|
575
|
-
abort "Error: #{e.message}"
|
|
363
|
+
def lit_create(output, *files)
|
|
364
|
+
opts = {
|
|
365
|
+
verbose: options[:verbose],
|
|
366
|
+
compress: options[:compress],
|
|
367
|
+
format: :lit,
|
|
368
|
+
language_id: parse_language_id(options[:language_id]),
|
|
369
|
+
}
|
|
370
|
+
run_dispatcher(:create, output, files, **opts)
|
|
576
371
|
end
|
|
577
372
|
|
|
578
|
-
# OAB commands
|
|
579
|
-
desc "oab-extract INPUT OUTPUT",
|
|
373
|
+
# OAB legacy commands
|
|
374
|
+
desc "oab-extract INPUT OUTPUT",
|
|
375
|
+
"Extract OAB file (legacy, use: extract --format oab INPUT --output OUTPUT)"
|
|
580
376
|
option :base, type: :string, desc: "Base file for incremental patch"
|
|
581
377
|
option :verbose, type: :boolean, aliases: "-v",
|
|
582
378
|
desc: "Enable verbose output"
|
|
583
379
|
def oab_extract(input, output)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
output)
|
|
592
|
-
puts "Applied patch: #{input} + #{options[:base]} -> #{output} (#{bytes} bytes)"
|
|
593
|
-
else
|
|
594
|
-
puts "Extracting: #{input} -> #{output}" if options[:verbose]
|
|
595
|
-
bytes = decompressor.decompress(input, output)
|
|
596
|
-
puts "Extracted #{input} -> #{output} (#{bytes} bytes)"
|
|
597
|
-
end
|
|
598
|
-
rescue Error => e
|
|
599
|
-
abort "Error: #{e.message}"
|
|
380
|
+
opts = {
|
|
381
|
+
verbose: options[:verbose],
|
|
382
|
+
output: output,
|
|
383
|
+
format: :oab,
|
|
384
|
+
base_file: options[:base],
|
|
385
|
+
}
|
|
386
|
+
run_dispatcher(:extract, input, nil, **opts)
|
|
600
387
|
end
|
|
601
388
|
|
|
602
|
-
desc "oab-
|
|
603
|
-
|
|
604
|
-
option :block_size, type: :numeric, default: 32_768,
|
|
605
|
-
desc: "Block size (default: 32KB)"
|
|
389
|
+
desc "oab-info FILE",
|
|
390
|
+
"Show OAB file information (legacy, use: info --format oab FILE)"
|
|
606
391
|
option :verbose, type: :boolean, aliases: "-v",
|
|
607
392
|
desc: "Enable verbose output"
|
|
608
|
-
def
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
compressor = OAB::Compressor.new
|
|
612
|
-
|
|
613
|
-
if options[:base]
|
|
614
|
-
puts "Creating patch: #{input} (base: #{options[:base]}) -> #{output}" if options[:verbose]
|
|
615
|
-
bytes = compressor.compress_incremental(input, options[:base], output,
|
|
616
|
-
block_size: options[:block_size])
|
|
617
|
-
puts "Created patch: #{output} (#{bytes} bytes)"
|
|
618
|
-
else
|
|
619
|
-
puts "Compressing: #{input} -> #{output}" if options[:verbose]
|
|
620
|
-
bytes = compressor.compress(input, output,
|
|
621
|
-
block_size: options[:block_size])
|
|
622
|
-
puts "Created #{output} (#{bytes} bytes)"
|
|
623
|
-
end
|
|
624
|
-
rescue Error => e
|
|
625
|
-
abort "Error: #{e.message}"
|
|
393
|
+
def oab_info(file)
|
|
394
|
+
run_with_format(:info, :oab, file, verbose: options[:verbose])
|
|
626
395
|
end
|
|
627
396
|
|
|
628
|
-
desc "oab-
|
|
397
|
+
desc "oab-create INPUT OUTPUT",
|
|
398
|
+
"Create OAB file (legacy, use: create --format oab OUTPUT INPUT)"
|
|
399
|
+
option :base, type: :string, desc: "Base file for incremental patch"
|
|
400
|
+
option :block_size, type: :numeric, default: 32_768, desc: "Block size"
|
|
629
401
|
option :verbose, type: :boolean, aliases: "-v",
|
|
630
402
|
desc: "Enable verbose output"
|
|
631
|
-
def
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
header_data = io_system.read(handle, 28) # Read full patch header size
|
|
640
|
-
io_system.close(handle)
|
|
641
|
-
|
|
642
|
-
# Try to parse as full header first
|
|
643
|
-
if header_data.length >= 16
|
|
644
|
-
full_header = Binary::OABStructures::FullHeader.read(header_data[0,
|
|
645
|
-
16])
|
|
646
|
-
|
|
647
|
-
if full_header.valid?
|
|
648
|
-
puts "OAB File Information (Full)"
|
|
649
|
-
puts "=" * 50
|
|
650
|
-
puts "Filename: #{file}"
|
|
651
|
-
puts "Version: #{full_header.version_hi}.#{full_header.version_lo}"
|
|
652
|
-
puts "Block size: #{full_header.block_max} bytes"
|
|
653
|
-
puts "Target size: #{full_header.target_size} bytes"
|
|
654
|
-
elsif header_data.length >= 28
|
|
655
|
-
# Try as patch header
|
|
656
|
-
patch_header = Binary::OABStructures::PatchHeader.read(header_data)
|
|
657
|
-
|
|
658
|
-
if patch_header.valid?
|
|
659
|
-
puts "OAB File Information (Patch)"
|
|
660
|
-
puts "=" * 50
|
|
661
|
-
puts "Filename: #{file}"
|
|
662
|
-
puts "Version: #{patch_header.version_hi}.#{patch_header.version_lo}"
|
|
663
|
-
puts "Block size: #{patch_header.block_max} bytes"
|
|
664
|
-
puts "Source size: #{patch_header.source_size} bytes"
|
|
665
|
-
puts "Target size: #{patch_header.target_size} bytes"
|
|
666
|
-
puts "Source CRC: 0x#{patch_header.source_crc.to_s(16)}"
|
|
667
|
-
puts "Target CRC: 0x#{patch_header.target_crc.to_s(16)}"
|
|
668
|
-
else
|
|
669
|
-
abort "Error: Not a valid OAB file"
|
|
670
|
-
end
|
|
671
|
-
else
|
|
672
|
-
abort "Error: Not a valid OAB file"
|
|
673
|
-
end
|
|
674
|
-
else
|
|
675
|
-
abort "Error: File too small to be OAB"
|
|
676
|
-
end
|
|
677
|
-
rescue StandardError => e
|
|
678
|
-
io_system.close(handle) if handle
|
|
679
|
-
abort "Error: #{e.message}"
|
|
680
|
-
end
|
|
681
|
-
rescue Error => e
|
|
682
|
-
abort "Error: #{e.message}"
|
|
403
|
+
def oab_create(input, output)
|
|
404
|
+
opts = {
|
|
405
|
+
verbose: options[:verbose],
|
|
406
|
+
format: :oab,
|
|
407
|
+
block_size: options[:block_size],
|
|
408
|
+
base_file: options[:base],
|
|
409
|
+
}
|
|
410
|
+
run_dispatcher(:create, output, [input], **opts)
|
|
683
411
|
end
|
|
684
412
|
|
|
685
413
|
desc "version", "Show version information"
|
|
@@ -689,88 +417,109 @@ module Cabriolet
|
|
|
689
417
|
|
|
690
418
|
private
|
|
691
419
|
|
|
692
|
-
|
|
693
|
-
|
|
420
|
+
# Run command with unified dispatcher
|
|
421
|
+
#
|
|
422
|
+
# @param command [Symbol] Command to execute
|
|
423
|
+
# @param file [String] File path
|
|
424
|
+
# @param args [Array] Additional arguments
|
|
425
|
+
def run_dispatcher(command, file, *args, **options)
|
|
426
|
+
setup_verbose(options[:verbose])
|
|
427
|
+
|
|
428
|
+
dispatcher = Commands::CommandDispatcher.new(**options)
|
|
429
|
+
dispatcher.dispatch(command, file, *args, **options)
|
|
694
430
|
end
|
|
695
431
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
cabinet.folders.each_with_index do |folder, idx|
|
|
709
|
-
puts " [#{idx}] #{folder.compression_name} (#{folder.num_blocks} blocks)"
|
|
710
|
-
end
|
|
711
|
-
puts ""
|
|
712
|
-
|
|
713
|
-
puts "Files:"
|
|
714
|
-
cabinet.files.each do |f|
|
|
715
|
-
puts " #{f.filename}"
|
|
716
|
-
puts " Size: #{f.length} bytes"
|
|
717
|
-
puts " Modified: #{f.modification_time}" if f.modification_time
|
|
718
|
-
puts " Attributes: #{file_attributes(f)}"
|
|
719
|
-
end
|
|
432
|
+
# Run command with explicit format override
|
|
433
|
+
#
|
|
434
|
+
# @param command [Symbol] Command to execute
|
|
435
|
+
# @param format [Symbol] Format to force
|
|
436
|
+
# @param file [String] File path
|
|
437
|
+
# @param args [Array] Additional arguments
|
|
438
|
+
def run_with_format(command, format, file, *args, **options)
|
|
439
|
+
setup_verbose(options[:verbose])
|
|
440
|
+
options[:format] = format.to_s
|
|
441
|
+
|
|
442
|
+
dispatcher = Commands::CommandDispatcher.new(**options)
|
|
443
|
+
dispatcher.dispatch(command, file, *args, **options)
|
|
720
444
|
end
|
|
721
445
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
puts " Chunk Size: #{chm.chunk_size}"
|
|
745
|
-
puts " First PMGL: #{chm.first_pmgl}"
|
|
746
|
-
puts " Last PMGL: #{chm.last_pmgl}"
|
|
747
|
-
puts ""
|
|
748
|
-
puts "Sections:"
|
|
749
|
-
puts " Section 0 (Uncompressed): offset #{chm.sec0.offset}"
|
|
750
|
-
puts " Section 1 (MSCompressed): LZX compression"
|
|
751
|
-
puts ""
|
|
752
|
-
|
|
753
|
-
regular_files = chm.all_files
|
|
754
|
-
system_files = chm.all_sysfiles
|
|
755
|
-
|
|
756
|
-
puts "Files: #{regular_files.length} regular, #{system_files.length} system"
|
|
757
|
-
puts ""
|
|
758
|
-
puts "Regular Files:"
|
|
759
|
-
regular_files.each do |f|
|
|
760
|
-
section_name = f.section.id.zero? ? "Sec0" : "Sec1"
|
|
761
|
-
puts " #{f.filename}"
|
|
762
|
-
puts " Size: #{f.length} bytes (#{section_name})"
|
|
446
|
+
# Detect format from output file extension
|
|
447
|
+
#
|
|
448
|
+
# @param output [String] Output file path
|
|
449
|
+
# @param manual_format [String, nil] Manually specified format
|
|
450
|
+
# @return [Symbol] Detected format symbol
|
|
451
|
+
def detect_format_from_output(output, manual_format)
|
|
452
|
+
return manual_format.to_sym if manual_format
|
|
453
|
+
|
|
454
|
+
ext = File.extname(output).downcase
|
|
455
|
+
format_map = {
|
|
456
|
+
".cab" => :cab,
|
|
457
|
+
".chm" => :chm,
|
|
458
|
+
".hlp" => :hlp,
|
|
459
|
+
".lit" => :lit,
|
|
460
|
+
".oab" => :oab,
|
|
461
|
+
"._" => :szdd, # SZDD ends with underscore
|
|
462
|
+
".kwj" => :kwaj,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# Handle SZDD specially (ends with _)
|
|
466
|
+
if output.end_with?("_")
|
|
467
|
+
return :szdd
|
|
763
468
|
end
|
|
764
469
|
|
|
765
|
-
|
|
470
|
+
format_map[ext] || :cab # Default to CAB
|
|
471
|
+
end
|
|
766
472
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
473
|
+
# Normalize create options for different formats
|
|
474
|
+
#
|
|
475
|
+
# @param options [Hash] Raw options from Thor
|
|
476
|
+
# @return [Hash] Normalized options
|
|
477
|
+
def normalize_create_options(options)
|
|
478
|
+
normalized = {}
|
|
479
|
+
options.each do |key, value|
|
|
480
|
+
next if value.nil?
|
|
481
|
+
|
|
482
|
+
case key.to_s
|
|
483
|
+
when "szdd_format"
|
|
484
|
+
normalized[:szdd_format] = value.to_sym
|
|
485
|
+
when "kwaj_compression"
|
|
486
|
+
normalized[:compression] = value
|
|
487
|
+
when "kwaj_filename"
|
|
488
|
+
normalized[:filename] = value
|
|
489
|
+
when "hlp_format"
|
|
490
|
+
normalized[:hlp_format] = value.to_sym
|
|
491
|
+
when "language_id"
|
|
492
|
+
normalized[:language_id] = parse_language_id(value)
|
|
493
|
+
when "lit_version"
|
|
494
|
+
normalized[:version] = value
|
|
495
|
+
when "compress"
|
|
496
|
+
# Keep as-is for HLP/LIT
|
|
497
|
+
normalized[:compress] = value
|
|
498
|
+
else
|
|
499
|
+
normalized[key.to_sym] = value
|
|
500
|
+
end
|
|
773
501
|
end
|
|
502
|
+
normalized
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Parse language ID from string
|
|
506
|
+
#
|
|
507
|
+
# @param value [String, Integer, nil] Language ID value
|
|
508
|
+
# @return [Integer] Parsed language ID
|
|
509
|
+
def parse_language_id(value)
|
|
510
|
+
return 0x409 if value.nil? # Default to English
|
|
511
|
+
|
|
512
|
+
if value.is_a?(Integer)
|
|
513
|
+
value
|
|
514
|
+
elsif value.start_with?("0x")
|
|
515
|
+
value.to_i(16)
|
|
516
|
+
else
|
|
517
|
+
value.to_i
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def setup_verbose(verbose)
|
|
522
|
+
Cabriolet.verbose = verbose
|
|
774
523
|
end
|
|
775
524
|
end
|
|
776
525
|
end
|