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
|
@@ -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
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "guid_generator"
|
|
4
|
+
require_relative "content_type_detector"
|
|
5
|
+
require_relative "directory_builder"
|
|
6
|
+
require_relative "structure_builder"
|
|
7
|
+
require_relative "header_writer"
|
|
8
|
+
require_relative "piece_builder"
|
|
9
|
+
require_relative "content_encoder"
|
|
10
|
+
|
|
3
11
|
module Cabriolet
|
|
4
12
|
module LIT
|
|
5
13
|
# Compressor creates LIT eBook files
|
|
@@ -10,15 +18,15 @@ module Cabriolet
|
|
|
10
18
|
# NOTE: This implementation creates non-encrypted LIT files only.
|
|
11
19
|
# DES encryption (DRM protection) is not implemented.
|
|
12
20
|
class Compressor
|
|
13
|
-
attr_reader :io_system
|
|
14
|
-
attr_accessor :files
|
|
21
|
+
attr_reader :io_system, :files
|
|
15
22
|
|
|
16
23
|
# Initialize a new LIT compressor
|
|
17
24
|
#
|
|
18
|
-
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for
|
|
19
|
-
#
|
|
20
|
-
def initialize(io_system = nil)
|
|
25
|
+
# @param io_system [System::IOSystem, nil] Custom I/O system or nil for default
|
|
26
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
|
|
27
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
21
28
|
@io_system = io_system || System::IOSystem.new
|
|
29
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
22
30
|
@files = []
|
|
23
31
|
end
|
|
24
32
|
|
|
@@ -27,8 +35,7 @@ module Cabriolet
|
|
|
27
35
|
# @param source_path [String] Path to the source file
|
|
28
36
|
# @param lit_path [String] Path within the LIT archive
|
|
29
37
|
# @param options [Hash] Options for the file
|
|
30
|
-
# @option options [Boolean] :compress Whether to compress the file
|
|
31
|
-
# (default: true)
|
|
38
|
+
# @option options [Boolean] :compress Whether to compress the file (default: true)
|
|
32
39
|
# @return [void]
|
|
33
40
|
def add_file(source_path, lit_path, **options)
|
|
34
41
|
compress = options.fetch(:compress, true)
|
|
@@ -45,209 +52,153 @@ module Cabriolet
|
|
|
45
52
|
# @param output_file [String] Path to output LIT file
|
|
46
53
|
# @param options [Hash] Generation options
|
|
47
54
|
# @option options [Integer] :version LIT format version (default: 1)
|
|
48
|
-
# @option options [
|
|
49
|
-
#
|
|
55
|
+
# @option options [Integer] :language_id Language ID (default: 0x409 English)
|
|
56
|
+
# @option options [Integer] :creator_id Creator ID (default: 0)
|
|
50
57
|
# @return [Integer] Bytes written to output file
|
|
51
|
-
# @raise [Errors::CompressionError] if
|
|
52
|
-
# @raise [NotImplementedError] if encryption is requested
|
|
58
|
+
# @raise [Errors::CompressionError] if compression fails
|
|
53
59
|
def generate(output_file, **options)
|
|
54
60
|
version = options.fetch(:version, 1)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if encrypt
|
|
58
|
-
raise NotImplementedError,
|
|
59
|
-
"DES encryption is not implemented. " \
|
|
60
|
-
"LIT files will be created without encryption."
|
|
61
|
-
end
|
|
61
|
+
language_id = options.fetch(:language_id, 0x409)
|
|
62
|
+
creator_id = options.fetch(:creator_id, 0)
|
|
62
63
|
|
|
63
64
|
raise ArgumentError, "No files added to archive" if @files.empty?
|
|
65
|
+
raise ArgumentError, "Version must be 1" unless version == 1
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
# Prepare file data
|
|
68
|
+
file_data = prepare_files
|
|
69
|
+
|
|
70
|
+
# Build LIT structure
|
|
71
|
+
structure_builder = StructureBuilder.new(
|
|
72
|
+
io_system: @io_system,
|
|
73
|
+
version: version,
|
|
74
|
+
language_id: language_id,
|
|
75
|
+
creator_id: creator_id,
|
|
76
|
+
)
|
|
77
|
+
lit_structure = structure_builder.build(file_data)
|
|
66
78
|
|
|
79
|
+
# Write to output file
|
|
80
|
+
output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
|
|
67
81
|
begin
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# Write header
|
|
72
|
-
header_bytes = write_header(
|
|
73
|
-
output_handle,
|
|
74
|
-
version,
|
|
75
|
-
file_data.size,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
# Write file entries
|
|
79
|
-
entries_bytes = write_file_entries(output_handle, file_data)
|
|
80
|
-
|
|
81
|
-
# Write file contents
|
|
82
|
-
content_bytes = write_file_contents(
|
|
83
|
-
output_handle,
|
|
84
|
-
file_data,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
header_bytes + entries_bytes + content_bytes
|
|
82
|
+
bytes_written = write_lit_file(output_handle, lit_structure)
|
|
83
|
+
bytes_written
|
|
88
84
|
ensure
|
|
89
|
-
@io_system.close(output_handle)
|
|
85
|
+
@io_system.close(output_handle)
|
|
90
86
|
end
|
|
91
87
|
end
|
|
92
88
|
|
|
93
89
|
private
|
|
94
90
|
|
|
95
|
-
#
|
|
91
|
+
# Write complete LIT file
|
|
96
92
|
#
|
|
97
|
-
# @
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
compress = file_info[:compress]
|
|
93
|
+
# @param output_handle [System::FileHandle] Output file handle
|
|
94
|
+
# @param structure [Hash] LIT structure
|
|
95
|
+
# @return [Integer] Bytes written
|
|
96
|
+
def write_lit_file(output_handle, structure)
|
|
97
|
+
header_writer = HeaderWriter.new(@io_system)
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
handle = @io_system.open(source, Constants::MODE_READ)
|
|
106
|
-
begin
|
|
107
|
-
size = @io_system.seek(handle, 0, Constants::SEEK_END)
|
|
108
|
-
@io_system.seek(handle, 0, Constants::SEEK_START)
|
|
109
|
-
data = @io_system.read(handle, size)
|
|
110
|
-
ensure
|
|
111
|
-
@io_system.close(handle)
|
|
112
|
-
end
|
|
99
|
+
bytes_written = 0
|
|
113
100
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
uncompressed_size: data.bytesize,
|
|
118
|
-
compress: compress,
|
|
119
|
-
}
|
|
120
|
-
end
|
|
121
|
-
end
|
|
101
|
+
# Write primary header (40 bytes)
|
|
102
|
+
bytes_written += header_writer.write_primary_header(output_handle,
|
|
103
|
+
structure)
|
|
122
104
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
header.version = version
|
|
133
|
-
header.flags = 0 # Not encrypted
|
|
134
|
-
header.file_count = file_count
|
|
135
|
-
header.header_size = 24 # Size of the header structure
|
|
136
|
-
|
|
137
|
-
header_data = header.to_binary_s
|
|
138
|
-
written = @io_system.write(output_handle, header_data)
|
|
139
|
-
|
|
140
|
-
unless written == header_data.bytesize
|
|
141
|
-
raise Errors::CompressionError,
|
|
142
|
-
"Failed to write LIT header"
|
|
143
|
-
end
|
|
105
|
+
# Write piece structures (5 * 16 bytes = 80 bytes)
|
|
106
|
+
bytes_written += header_writer.write_piece_structures(output_handle,
|
|
107
|
+
structure[:pieces])
|
|
108
|
+
|
|
109
|
+
# Write secondary header
|
|
110
|
+
bytes_written += header_writer.write_secondary_header(
|
|
111
|
+
output_handle,
|
|
112
|
+
structure[:secondary_header],
|
|
113
|
+
)
|
|
144
114
|
|
|
145
|
-
|
|
115
|
+
# Write piece data
|
|
116
|
+
bytes_written += write_piece_data(output_handle, structure)
|
|
117
|
+
|
|
118
|
+
bytes_written
|
|
146
119
|
end
|
|
147
120
|
|
|
148
|
-
# Write
|
|
121
|
+
# Write piece data
|
|
149
122
|
#
|
|
150
123
|
# @param output_handle [System::FileHandle] Output file handle
|
|
151
|
-
# @param
|
|
152
|
-
# @return [Integer]
|
|
153
|
-
def
|
|
124
|
+
# @param structure [Hash] LIT structure
|
|
125
|
+
# @return [Integer] Bytes written
|
|
126
|
+
def write_piece_data(output_handle, structure)
|
|
154
127
|
total_bytes = 0
|
|
155
|
-
current_offset = calculate_header_size(file_data)
|
|
156
|
-
|
|
157
|
-
file_data.each do |file_info|
|
|
158
|
-
# Compress or store data
|
|
159
|
-
if file_info[:compress]
|
|
160
|
-
compressed = compress_data(file_info[:data])
|
|
161
|
-
compressed_size = compressed.bytesize
|
|
162
|
-
flags = Binary::LITStructures::FileFlags::COMPRESSED
|
|
163
|
-
else
|
|
164
|
-
compressed = file_info[:data]
|
|
165
|
-
compressed_size = compressed.bytesize
|
|
166
|
-
flags = 0
|
|
167
|
-
end
|
|
168
128
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
129
|
+
# Write piece 0: File size information
|
|
130
|
+
piece0_data = PieceBuilder.build_piece0(structure[:file_data])
|
|
131
|
+
total_bytes += @io_system.write(output_handle, piece0_data)
|
|
132
|
+
|
|
133
|
+
# Write piece 1: Directory (IFCM structure)
|
|
134
|
+
piece1_data = PieceBuilder.build_piece1(structure[:directory])
|
|
135
|
+
total_bytes += @io_system.write(output_handle, piece1_data)
|
|
136
|
+
|
|
137
|
+
# Write piece 2: Index information
|
|
138
|
+
piece2_data = PieceBuilder.build_piece2
|
|
139
|
+
total_bytes += @io_system.write(output_handle, piece2_data)
|
|
140
|
+
|
|
141
|
+
# Write piece 3: GUID
|
|
142
|
+
total_bytes += @io_system.write(output_handle, structure[:piece3_guid])
|
|
143
|
+
|
|
144
|
+
# Write piece 4: GUID
|
|
145
|
+
total_bytes += @io_system.write(output_handle, structure[:piece4_guid])
|
|
146
|
+
|
|
147
|
+
# Write actual content data (after pieces, this is where files go)
|
|
148
|
+
total_bytes += write_content_data(output_handle, structure)
|
|
189
149
|
|
|
190
150
|
total_bytes
|
|
191
151
|
end
|
|
192
152
|
|
|
193
|
-
# Write file contents
|
|
153
|
+
# Write content data (actual file contents)
|
|
194
154
|
#
|
|
195
155
|
# @param output_handle [System::FileHandle] Output file handle
|
|
196
|
-
# @param
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
def write_file_contents(output_handle, file_data)
|
|
156
|
+
# @param structure [Hash] LIT structure
|
|
157
|
+
# @return [Integer] Bytes written
|
|
158
|
+
def write_content_data(output_handle, structure)
|
|
200
159
|
total_bytes = 0
|
|
201
160
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
file_info[:compressed_data],
|
|
206
|
-
)
|
|
207
|
-
total_bytes += written
|
|
161
|
+
# Write each file's content
|
|
162
|
+
structure[:file_data].each do |file_info|
|
|
163
|
+
total_bytes += @io_system.write(output_handle, file_info[:data])
|
|
208
164
|
end
|
|
209
165
|
|
|
210
|
-
|
|
211
|
-
|
|
166
|
+
# Write NameList
|
|
167
|
+
namelist_data = ContentEncoder.build_namelist_data(structure[:sections])
|
|
168
|
+
total_bytes += @io_system.write(output_handle, namelist_data)
|
|
212
169
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
# @return [Integer] Total header size in bytes
|
|
217
|
-
def calculate_header_size(file_data)
|
|
218
|
-
# Header: 24 bytes
|
|
219
|
-
header_size = 24
|
|
220
|
-
|
|
221
|
-
# File entries: variable size
|
|
222
|
-
file_data.each do |file_info|
|
|
223
|
-
# 4 bytes filename length + filename + 28 bytes metadata
|
|
224
|
-
header_size += 4 + file_info[:lit_path].bytesize + 28
|
|
225
|
-
end
|
|
170
|
+
# Write manifest
|
|
171
|
+
manifest_data = ContentEncoder.build_manifest_data(structure[:manifest])
|
|
172
|
+
total_bytes += @io_system.write(output_handle, manifest_data)
|
|
226
173
|
|
|
227
|
-
|
|
174
|
+
total_bytes
|
|
228
175
|
end
|
|
229
176
|
|
|
230
|
-
#
|
|
177
|
+
# Prepare file data for archiving
|
|
231
178
|
#
|
|
232
|
-
# @
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
begin
|
|
239
|
-
compressor = Compressors::LZX.new(
|
|
240
|
-
@io_system,
|
|
241
|
-
input_handle,
|
|
242
|
-
output_handle,
|
|
243
|
-
32_768,
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
compressor.compress
|
|
179
|
+
# @return [Array<Hash>] Array of file information hashes
|
|
180
|
+
def prepare_files
|
|
181
|
+
@files.map do |file_info|
|
|
182
|
+
source = file_info[:source]
|
|
183
|
+
lit_path = file_info[:lit_path]
|
|
184
|
+
compress = file_info[:compress]
|
|
247
185
|
|
|
248
|
-
|
|
186
|
+
# Read source file
|
|
187
|
+
handle = @io_system.open(source, Constants::MODE_READ)
|
|
188
|
+
begin
|
|
189
|
+
size = @io_system.seek(handle, 0, Constants::SEEK_END)
|
|
190
|
+
@io_system.seek(handle, 0, Constants::SEEK_START)
|
|
191
|
+
data = @io_system.read(handle, size)
|
|
192
|
+
ensure
|
|
193
|
+
@io_system.close(handle)
|
|
194
|
+
end
|
|
249
195
|
|
|
250
|
-
|
|
196
|
+
{
|
|
197
|
+
lit_path: lit_path,
|
|
198
|
+
data: data,
|
|
199
|
+
uncompressed_size: data.bytesize,
|
|
200
|
+
compress: compress,
|
|
201
|
+
}
|
|
251
202
|
end
|
|
252
203
|
end
|
|
253
204
|
end
|