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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +700 -38
  3. data/lib/cabriolet/algorithm_factory.rb +250 -0
  4. data/lib/cabriolet/base_compressor.rb +206 -0
  5. data/lib/cabriolet/binary/bitstream.rb +154 -14
  6. data/lib/cabriolet/binary/bitstream_writer.rb +129 -17
  7. data/lib/cabriolet/binary/chm_structures.rb +2 -2
  8. data/lib/cabriolet/binary/hlp_structures.rb +258 -37
  9. data/lib/cabriolet/binary/lit_structures.rb +231 -65
  10. data/lib/cabriolet/binary/oab_structures.rb +17 -1
  11. data/lib/cabriolet/cab/command_handler.rb +226 -0
  12. data/lib/cabriolet/cab/compressor.rb +35 -43
  13. data/lib/cabriolet/cab/decompressor.rb +14 -19
  14. data/lib/cabriolet/cab/extractor.rb +140 -31
  15. data/lib/cabriolet/chm/command_handler.rb +227 -0
  16. data/lib/cabriolet/chm/compressor.rb +7 -3
  17. data/lib/cabriolet/chm/decompressor.rb +39 -21
  18. data/lib/cabriolet/chm/parser.rb +5 -2
  19. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  20. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  21. data/lib/cabriolet/cli/command_registry.rb +83 -0
  22. data/lib/cabriolet/cli.rb +356 -607
  23. data/lib/cabriolet/compressors/base.rb +1 -1
  24. data/lib/cabriolet/compressors/lzx.rb +241 -54
  25. data/lib/cabriolet/compressors/mszip.rb +35 -3
  26. data/lib/cabriolet/compressors/quantum.rb +34 -45
  27. data/lib/cabriolet/decompressors/base.rb +1 -1
  28. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  29. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  30. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  31. data/lib/cabriolet/decompressors/quantum.rb +3 -2
  32. data/lib/cabriolet/errors.rb +3 -0
  33. data/lib/cabriolet/file_entry.rb +156 -0
  34. data/lib/cabriolet/file_manager.rb +144 -0
  35. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  36. data/lib/cabriolet/hlp/compressor.rb +28 -238
  37. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  38. data/lib/cabriolet/hlp/parser.rb +52 -101
  39. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  40. data/lib/cabriolet/hlp/quickhelp/compressor.rb +626 -0
  41. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  42. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  43. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  44. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  45. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  46. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  47. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  48. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  49. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  50. data/lib/cabriolet/huffman/tree.rb +85 -1
  51. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  52. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  53. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  54. data/lib/cabriolet/lit/command_handler.rb +221 -0
  55. data/lib/cabriolet/lit/compressor.rb +633 -38
  56. data/lib/cabriolet/lit/decompressor.rb +518 -152
  57. data/lib/cabriolet/lit/parser.rb +670 -0
  58. data/lib/cabriolet/models/hlp_file.rb +130 -29
  59. data/lib/cabriolet/models/hlp_header.rb +105 -17
  60. data/lib/cabriolet/models/lit_header.rb +212 -25
  61. data/lib/cabriolet/models/szdd_header.rb +10 -2
  62. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  63. data/lib/cabriolet/oab/command_handler.rb +257 -0
  64. data/lib/cabriolet/oab/compressor.rb +17 -8
  65. data/lib/cabriolet/oab/decompressor.rb +41 -10
  66. data/lib/cabriolet/offset_calculator.rb +81 -0
  67. data/lib/cabriolet/plugin.rb +233 -0
  68. data/lib/cabriolet/plugin_manager.rb +453 -0
  69. data/lib/cabriolet/plugin_validator.rb +422 -0
  70. data/lib/cabriolet/system/io_system.rb +3 -0
  71. data/lib/cabriolet/system/memory_handle.rb +17 -4
  72. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  73. data/lib/cabriolet/szdd/compressor.rb +15 -11
  74. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  75. data/lib/cabriolet/version.rb +1 -1
  76. data/lib/cabriolet.rb +67 -17
  77. 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
- def initialize(io_system = nil)
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 [Errors::ParseError] if the file is not a valid KWAJ
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 [Errors::DecompressionError] if decompression fails
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 [Errors::ParseError] if input is not valid KWAJ
94
- # @raise [Errors::DecompressionError] if decompression fails
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 [Errors::DecompressionError] if decompression fails
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 Errors::DecompressionError,
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 = Decompressors::LZSS.new(
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 [Errors::DecompressionError] LZH not yet implemented
218
+ # @raise [Cabriolet::Error] LZH not yet implemented
215
219
  def decompress_lzh(_input_handle, _output_handle)
216
- raise Errors::DecompressionError,
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 = Decompressors::MSZIP.new(
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