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
|
@@ -21,8 +21,10 @@ module Cabriolet
|
|
|
21
21
|
#
|
|
22
22
|
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
23
23
|
# default
|
|
24
|
-
|
|
24
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
25
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
25
26
|
@io_system = io_system || System::IOSystem.new
|
|
27
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
26
28
|
@parser = Parser.new(@io_system)
|
|
27
29
|
@buffer_size = DEFAULT_BUFFER_SIZE
|
|
28
30
|
end
|
|
@@ -31,7 +33,7 @@ module Cabriolet
|
|
|
31
33
|
#
|
|
32
34
|
# @param filename [String] Path to the KWAJ file
|
|
33
35
|
# @return [Models::KWAJHeader] Parsed header
|
|
34
|
-
# @raise [
|
|
36
|
+
# @raise [Cabriolet::ErrorParseError] if the file is not a valid KWAJ
|
|
35
37
|
def open(filename)
|
|
36
38
|
@parser.parse(filename)
|
|
37
39
|
end
|
|
@@ -52,7 +54,7 @@ module Cabriolet
|
|
|
52
54
|
# @param filename [String] Input filename
|
|
53
55
|
# @param output_path [String] Where to write the decompressed file
|
|
54
56
|
# @return [Integer] Number of bytes written
|
|
55
|
-
# @raise [
|
|
57
|
+
# @raise [Cabriolet::ErrorDecompressionError] if decompression fails
|
|
56
58
|
def extract(header, filename, output_path)
|
|
57
59
|
raise ArgumentError, "Header must not be nil" unless header
|
|
58
60
|
raise ArgumentError, "Output path must not be nil" unless output_path
|
|
@@ -90,8 +92,8 @@ module Cabriolet
|
|
|
90
92
|
# @param output_path [String, nil] Path to output file, or nil to
|
|
91
93
|
# auto-detect
|
|
92
94
|
# @return [Integer] Number of bytes written
|
|
93
|
-
# @raise [
|
|
94
|
-
# @raise [
|
|
95
|
+
# @raise [Cabriolet::ErrorParseError] if input is not valid KWAJ
|
|
96
|
+
# @raise [Cabriolet::ErrorDecompressionError] if decompression fails
|
|
95
97
|
def decompress(input_path, output_path = nil)
|
|
96
98
|
# Parse header
|
|
97
99
|
header = open(input_path)
|
|
@@ -134,7 +136,7 @@ module Cabriolet
|
|
|
134
136
|
# @param input_handle [System::FileHandle] Input handle
|
|
135
137
|
# @param output_handle [System::FileHandle] Output handle
|
|
136
138
|
# @return [Integer] Number of bytes written
|
|
137
|
-
# @raise [
|
|
139
|
+
# @raise [Cabriolet::ErrorDecompressionError] if decompression fails
|
|
138
140
|
def decompress_data(header, input_handle, output_handle)
|
|
139
141
|
case header.comp_type
|
|
140
142
|
when Constants::KWAJ_COMP_NONE
|
|
@@ -148,7 +150,7 @@ module Cabriolet
|
|
|
148
150
|
when Constants::KWAJ_COMP_MSZIP
|
|
149
151
|
decompress_mszip(input_handle, output_handle)
|
|
150
152
|
else
|
|
151
|
-
raise
|
|
153
|
+
raise Error,
|
|
152
154
|
"Unsupported compression type: #{header.comp_type}"
|
|
153
155
|
end
|
|
154
156
|
end
|
|
@@ -196,12 +198,14 @@ module Cabriolet
|
|
|
196
198
|
# @param output_handle [System::FileHandle] Output handle
|
|
197
199
|
# @return [Integer] Number of bytes written
|
|
198
200
|
def decompress_szdd(input_handle, output_handle)
|
|
199
|
-
decompressor =
|
|
201
|
+
decompressor = @algorithm_factory.create(
|
|
202
|
+
:lzss,
|
|
203
|
+
:decompressor,
|
|
200
204
|
@io_system,
|
|
201
205
|
input_handle,
|
|
202
206
|
output_handle,
|
|
203
207
|
@buffer_size,
|
|
204
|
-
Decompressors::LZSS::MODE_QBASIC,
|
|
208
|
+
mode: Decompressors::LZSS::MODE_QBASIC,
|
|
205
209
|
)
|
|
206
210
|
decompressor.decompress(0)
|
|
207
211
|
end
|
|
@@ -211,9 +215,9 @@ module Cabriolet
|
|
|
211
215
|
# @param input_handle [System::FileHandle] Input handle
|
|
212
216
|
# @param output_handle [System::FileHandle] Output handle
|
|
213
217
|
# @return [Integer] Number of bytes written
|
|
214
|
-
# @raise [
|
|
218
|
+
# @raise [Cabriolet::Error] LZH not yet implemented
|
|
215
219
|
def decompress_lzh(_input_handle, _output_handle)
|
|
216
|
-
raise
|
|
220
|
+
raise Error,
|
|
217
221
|
"LZH compression type is not yet implemented. " \
|
|
218
222
|
"This requires custom Huffman tree implementation."
|
|
219
223
|
end
|
|
@@ -224,7 +228,9 @@ module Cabriolet
|
|
|
224
228
|
# @param output_handle [System::FileHandle] Output handle
|
|
225
229
|
# @return [Integer] Number of bytes written
|
|
226
230
|
def decompress_mszip(input_handle, output_handle)
|
|
227
|
-
decompressor =
|
|
231
|
+
decompressor = @algorithm_factory.create(
|
|
232
|
+
Constants::COMP_TYPE_MSZIP,
|
|
233
|
+
:decompressor,
|
|
228
234
|
@io_system,
|
|
229
235
|
input_handle,
|
|
230
236
|
output_handle,
|
|
@@ -0,0 +1,221 @@
|
|
|
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 LIT
|
|
9
|
+
# Command handler for LIT (Microsoft Reader eBook) format
|
|
10
|
+
#
|
|
11
|
+
# This handler implements the unified command interface for LIT files,
|
|
12
|
+
# wrapping the existing LIT::Decompressor and LIT::Compressor classes.
|
|
13
|
+
# LIT files use LZX compression and may include DRM protection.
|
|
14
|
+
#
|
|
15
|
+
class CommandHandler < Commands::BaseCommandHandler
|
|
16
|
+
# List LIT file contents
|
|
17
|
+
#
|
|
18
|
+
# Displays information about the LIT file including version,
|
|
19
|
+
# language, and lists all contained files with their sizes.
|
|
20
|
+
#
|
|
21
|
+
# @param file [String] Path to the LIT file
|
|
22
|
+
# @param options [Hash] Additional options
|
|
23
|
+
# @option options [Boolean] :use_manifest Use manifest for original filenames
|
|
24
|
+
# @return [void]
|
|
25
|
+
def list(file, options = {})
|
|
26
|
+
validate_file_exists(file)
|
|
27
|
+
|
|
28
|
+
decompressor = Decompressor.new
|
|
29
|
+
lit_file = decompressor.open(file)
|
|
30
|
+
|
|
31
|
+
display_header(lit_file)
|
|
32
|
+
display_files(lit_file, decompressor, options)
|
|
33
|
+
|
|
34
|
+
decompressor.close(lit_file)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Extract files from LIT archive
|
|
38
|
+
#
|
|
39
|
+
# Extracts all files from the LIT file to the specified output directory.
|
|
40
|
+
# Uses manifest for filename restoration if available.
|
|
41
|
+
#
|
|
42
|
+
# @param file [String] Path to the LIT file
|
|
43
|
+
# @param output_dir [String] Output directory path (default: current directory)
|
|
44
|
+
# @param options [Hash] Additional options
|
|
45
|
+
# @option options [Boolean] :use_manifest Use manifest for filenames (default: true)
|
|
46
|
+
# @return [void]
|
|
47
|
+
def extract(file, output_dir = nil, options = {})
|
|
48
|
+
validate_file_exists(file)
|
|
49
|
+
|
|
50
|
+
output_dir ||= "."
|
|
51
|
+
output_dir = ensure_output_dir(output_dir)
|
|
52
|
+
|
|
53
|
+
decompressor = Decompressor.new
|
|
54
|
+
lit_file = decompressor.open(file)
|
|
55
|
+
|
|
56
|
+
use_manifest = options.fetch(:use_manifest, true)
|
|
57
|
+
count = decompressor.extract_all(lit_file, output_dir,
|
|
58
|
+
use_manifest: use_manifest)
|
|
59
|
+
|
|
60
|
+
decompressor.close(lit_file)
|
|
61
|
+
puts "Extracted #{count} file(s) to #{output_dir}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create a new LIT archive
|
|
65
|
+
#
|
|
66
|
+
# Creates a LIT file from HTML source files.
|
|
67
|
+
# Non-encrypted LIT files are created (DRM not supported).
|
|
68
|
+
#
|
|
69
|
+
# @param output [String] Output LIT file path
|
|
70
|
+
# @param files [Array<String>] List of input files to add
|
|
71
|
+
# @param options [Hash] Additional options
|
|
72
|
+
# @option options [Integer] :language_id Language ID (default: 0x409 English)
|
|
73
|
+
# @option options [Integer] :version LIT format version (default: 1)
|
|
74
|
+
# @return [void]
|
|
75
|
+
# @raise [ArgumentError] if no files specified
|
|
76
|
+
def create(output, files = [], options = {})
|
|
77
|
+
raise ArgumentError, "No files specified" if files.empty?
|
|
78
|
+
|
|
79
|
+
files.each do |f|
|
|
80
|
+
raise ArgumentError, "File does not exist: #{f}" unless File.exist?(f)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
language_id = options[:language_id] || 0x409
|
|
84
|
+
version = options[:version] || 1
|
|
85
|
+
|
|
86
|
+
compressor = Compressor.new
|
|
87
|
+
files.each do |f|
|
|
88
|
+
# Default to adding with compression
|
|
89
|
+
lit_path = "/#{File.basename(f)}"
|
|
90
|
+
compressor.add_file(f, lit_path, compress: true)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "Creating #{output} with #{files.size} file(s) (v#{version}, lang: 0x#{Integer(language_id).to_s(16)})" if verbose?
|
|
94
|
+
bytes = compressor.generate(output, version: version,
|
|
95
|
+
language_id: language_id)
|
|
96
|
+
puts "Created #{output} (#{bytes} bytes, #{files.size} files)"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Display detailed LIT file information
|
|
100
|
+
#
|
|
101
|
+
# Shows comprehensive information about the LIT structure,
|
|
102
|
+
# including sections, manifest, and files.
|
|
103
|
+
#
|
|
104
|
+
# @param file [String] Path to the LIT file
|
|
105
|
+
# @param options [Hash] Additional options (unused)
|
|
106
|
+
# @return [void]
|
|
107
|
+
def info(file, _options = {})
|
|
108
|
+
validate_file_exists(file)
|
|
109
|
+
|
|
110
|
+
decompressor = Decompressor.new
|
|
111
|
+
lit_file = decompressor.open(file)
|
|
112
|
+
|
|
113
|
+
display_lit_info(lit_file)
|
|
114
|
+
|
|
115
|
+
decompressor.close(lit_file)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Test LIT file integrity
|
|
119
|
+
#
|
|
120
|
+
# Verifies the LIT file structure.
|
|
121
|
+
#
|
|
122
|
+
# @param file [String] Path to the LIT file
|
|
123
|
+
# @param options [Hash] Additional options (unused)
|
|
124
|
+
# @return [void]
|
|
125
|
+
def test(file, _options = {})
|
|
126
|
+
validate_file_exists(file)
|
|
127
|
+
|
|
128
|
+
decompressor = Decompressor.new
|
|
129
|
+
lit_file = decompressor.open(file)
|
|
130
|
+
|
|
131
|
+
puts "Testing #{file}..."
|
|
132
|
+
# Check for DRM
|
|
133
|
+
if lit_file.encrypted?
|
|
134
|
+
puts "WARNING: LIT file is DRM-encrypted (level: #{lit_file.drm_level})"
|
|
135
|
+
puts "Encryption is not supported by this implementation"
|
|
136
|
+
else
|
|
137
|
+
puts "OK: LIT file structure is valid (#{lit_file.directory.entries.size} files)"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
decompressor.close(lit_file)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Display LIT header information
|
|
146
|
+
#
|
|
147
|
+
# @param lit_file [Models::LITFile] The LIT file object
|
|
148
|
+
# @return [void]
|
|
149
|
+
def display_header(lit_file)
|
|
150
|
+
puts "LIT File: #{File.basename(lit_file.instance_variable_get(:@filename) || 'unknown')}"
|
|
151
|
+
puts "Version: #{lit_file.version}"
|
|
152
|
+
puts "Language ID: 0x#{Integer(lit_file.language_id).to_s(16).upcase}"
|
|
153
|
+
puts "DRM Protected: #{lit_file.encrypted? ? 'Yes' : 'No'}"
|
|
154
|
+
puts "\nFiles:"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Display list of files in LIT
|
|
158
|
+
#
|
|
159
|
+
# @param lit_file [Models::LITFile] The LIT file object
|
|
160
|
+
# @param decompressor [Decompressor] The decompressor instance
|
|
161
|
+
# @param options [Hash] Display options
|
|
162
|
+
# @return [void]
|
|
163
|
+
def display_files(lit_file, decompressor, options)
|
|
164
|
+
use_manifest = options.fetch(:use_manifest, true)
|
|
165
|
+
files = decompressor.list_files(lit_file, use_manifest: use_manifest)
|
|
166
|
+
|
|
167
|
+
files.each do |f|
|
|
168
|
+
name = f[:original_name] || f[:internal_name]
|
|
169
|
+
size = f[:size]
|
|
170
|
+
content_type = f[:content_type]
|
|
171
|
+
|
|
172
|
+
line = " #{name} (#{size} bytes)"
|
|
173
|
+
line += " [#{content_type}]" if content_type && use_manifest
|
|
174
|
+
puts line
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Display comprehensive LIT information
|
|
179
|
+
#
|
|
180
|
+
# @param lit_file [Models::LITFile] The LIT file object
|
|
181
|
+
# @return [void]
|
|
182
|
+
def display_lit_info(lit_file)
|
|
183
|
+
puts "LIT File Information"
|
|
184
|
+
puts "=" * 50
|
|
185
|
+
|
|
186
|
+
filename = lit_file.instance_variable_get(:@filename)
|
|
187
|
+
puts "Filename: #{filename || 'unknown'}"
|
|
188
|
+
puts "Version: #{lit_file.version}"
|
|
189
|
+
puts "Language ID: 0x#{Integer(lit_file.language_id).to_s(16).upcase}"
|
|
190
|
+
puts "Creator ID: #{lit_file.creator_id}"
|
|
191
|
+
puts "Timestamp: #{Time.at(lit_file.timestamp)}" if lit_file.respond_to?(:timestamp)
|
|
192
|
+
|
|
193
|
+
puts ""
|
|
194
|
+
if lit_file.encrypted?
|
|
195
|
+
puts "DRM Protection:"
|
|
196
|
+
puts " Status: ENCRYPTED"
|
|
197
|
+
puts " Level: #{lit_file.drm_level}"
|
|
198
|
+
puts " WARNING: DRM decryption is not supported"
|
|
199
|
+
else
|
|
200
|
+
puts "DRM Protection: None"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
puts ""
|
|
204
|
+
puts "Sections: #{lit_file.sections.size}"
|
|
205
|
+
lit_file.sections.compact.each_with_index do |section, idx|
|
|
206
|
+
puts " [#{idx}] #{section.name}"
|
|
207
|
+
puts " Transforms: #{section.transforms.join(', ')}" if section.transforms.any?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
puts ""
|
|
211
|
+
puts "Files: #{lit_file.directory.entries.size - 1}" # Exclude root entry
|
|
212
|
+
|
|
213
|
+
# Display manifest if available
|
|
214
|
+
if lit_file.manifest
|
|
215
|
+
puts ""
|
|
216
|
+
puts "Manifest mappings: #{lit_file.manifest.mappings.size}"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|