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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +703 -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 +167 -16
  6. data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
  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 +108 -84
  13. data/lib/cabriolet/cab/decompressor.rb +16 -20
  14. data/lib/cabriolet/cab/extractor.rb +142 -66
  15. data/lib/cabriolet/cab/file_compression_work.rb +52 -0
  16. data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
  17. data/lib/cabriolet/checksum.rb +49 -0
  18. data/lib/cabriolet/chm/command_handler.rb +227 -0
  19. data/lib/cabriolet/chm/compressor.rb +7 -3
  20. data/lib/cabriolet/chm/decompressor.rb +39 -21
  21. data/lib/cabriolet/chm/parser.rb +5 -2
  22. data/lib/cabriolet/cli/base_command_handler.rb +127 -0
  23. data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
  24. data/lib/cabriolet/cli/command_registry.rb +83 -0
  25. data/lib/cabriolet/cli.rb +356 -607
  26. data/lib/cabriolet/collections/file_collection.rb +175 -0
  27. data/lib/cabriolet/compressors/base.rb +1 -1
  28. data/lib/cabriolet/compressors/lzx.rb +241 -54
  29. data/lib/cabriolet/compressors/mszip.rb +35 -3
  30. data/lib/cabriolet/compressors/quantum.rb +36 -95
  31. data/lib/cabriolet/decompressors/base.rb +1 -1
  32. data/lib/cabriolet/decompressors/lzss.rb +13 -3
  33. data/lib/cabriolet/decompressors/lzx.rb +70 -33
  34. data/lib/cabriolet/decompressors/mszip.rb +126 -39
  35. data/lib/cabriolet/decompressors/quantum.rb +83 -53
  36. data/lib/cabriolet/errors.rb +3 -0
  37. data/lib/cabriolet/extraction/base_extractor.rb +88 -0
  38. data/lib/cabriolet/extraction/extractor.rb +171 -0
  39. data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
  40. data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
  41. data/lib/cabriolet/file_entry.rb +156 -0
  42. data/lib/cabriolet/file_manager.rb +144 -0
  43. data/lib/cabriolet/format_base.rb +79 -0
  44. data/lib/cabriolet/hlp/command_handler.rb +282 -0
  45. data/lib/cabriolet/hlp/compressor.rb +28 -238
  46. data/lib/cabriolet/hlp/decompressor.rb +107 -147
  47. data/lib/cabriolet/hlp/parser.rb +52 -101
  48. data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
  49. data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
  50. data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
  51. data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
  52. data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
  53. data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
  54. data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
  55. data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
  56. data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
  57. data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
  58. data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
  59. data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
  60. data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
  61. data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
  62. data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
  63. data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
  64. data/lib/cabriolet/huffman/encoder.rb +15 -12
  65. data/lib/cabriolet/huffman/tree.rb +85 -1
  66. data/lib/cabriolet/kwaj/command_handler.rb +213 -0
  67. data/lib/cabriolet/kwaj/compressor.rb +7 -3
  68. data/lib/cabriolet/kwaj/decompressor.rb +18 -12
  69. data/lib/cabriolet/lit/command_handler.rb +221 -0
  70. data/lib/cabriolet/lit/compressor.rb +119 -168
  71. data/lib/cabriolet/lit/content_encoder.rb +76 -0
  72. data/lib/cabriolet/lit/content_type_detector.rb +50 -0
  73. data/lib/cabriolet/lit/decompressor.rb +518 -152
  74. data/lib/cabriolet/lit/directory_builder.rb +153 -0
  75. data/lib/cabriolet/lit/guid_generator.rb +16 -0
  76. data/lib/cabriolet/lit/header_writer.rb +124 -0
  77. data/lib/cabriolet/lit/parser.rb +670 -0
  78. data/lib/cabriolet/lit/piece_builder.rb +74 -0
  79. data/lib/cabriolet/lit/structure_builder.rb +252 -0
  80. data/lib/cabriolet/models/hlp_file.rb +130 -29
  81. data/lib/cabriolet/models/hlp_header.rb +105 -17
  82. data/lib/cabriolet/models/lit_header.rb +212 -25
  83. data/lib/cabriolet/models/szdd_header.rb +10 -2
  84. data/lib/cabriolet/models/winhelp_header.rb +127 -0
  85. data/lib/cabriolet/oab/command_handler.rb +257 -0
  86. data/lib/cabriolet/oab/compressor.rb +17 -8
  87. data/lib/cabriolet/oab/decompressor.rb +41 -10
  88. data/lib/cabriolet/offset_calculator.rb +81 -0
  89. data/lib/cabriolet/plugin.rb +233 -0
  90. data/lib/cabriolet/plugin_manager.rb +453 -0
  91. data/lib/cabriolet/plugin_validator.rb +422 -0
  92. data/lib/cabriolet/quantum_shared.rb +105 -0
  93. data/lib/cabriolet/system/io_system.rb +3 -0
  94. data/lib/cabriolet/system/memory_handle.rb +17 -4
  95. data/lib/cabriolet/szdd/command_handler.rb +217 -0
  96. data/lib/cabriolet/szdd/compressor.rb +15 -11
  97. data/lib/cabriolet/szdd/decompressor.rb +18 -9
  98. data/lib/cabriolet/version.rb +1 -1
  99. data/lib/cabriolet.rb +181 -20
  100. metadata +69 -4
  101. data/lib/cabriolet/auto.rb +0 -173
  102. 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
- 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
@@ -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
- # default
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 [Boolean] :encrypt Whether to encrypt (not supported,
49
- # raises error)
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 generation fails
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
- encrypt = options.fetch(:encrypt, false)
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
- output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
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
- # Prepare file data
69
- file_data = prepare_files
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) if output_handle
85
+ @io_system.close(output_handle)
90
86
  end
91
87
  end
92
88
 
93
89
  private
94
90
 
95
- # Prepare file data for archiving
91
+ # Write complete LIT file
96
92
  #
97
- # @return [Array<Hash>] Array of file information hashes
98
- def prepare_files
99
- @files.map do |file_info|
100
- source = file_info[:source]
101
- lit_path = file_info[:lit_path]
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
- # Read source file
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
- lit_path: lit_path,
116
- data: data,
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
- # Write LIT header
124
- #
125
- # @param output_handle [System::FileHandle] Output file handle
126
- # @param version [Integer] LIT format version
127
- # @param file_count [Integer] Number of files
128
- # @return [Integer] Number of bytes written
129
- def write_header(output_handle, version, file_count)
130
- header = Binary::LITStructures::LITHeader.new
131
- header.signature = Binary::LITStructures::SIGNATURE
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
- written
115
+ # Write piece data
116
+ bytes_written += write_piece_data(output_handle, structure)
117
+
118
+ bytes_written
146
119
  end
147
120
 
148
- # Write file entries directory
121
+ # Write piece data
149
122
  #
150
123
  # @param output_handle [System::FileHandle] Output file handle
151
- # @param file_data [Array<Hash>] Array of file information
152
- # @return [Integer] Number of bytes written
153
- def write_file_entries(output_handle, file_data)
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
- # Store compressed data for later writing
170
- file_info[:compressed_data] = compressed
171
- file_info[:compressed_size] = compressed_size
172
- file_info[:offset] = current_offset
173
-
174
- # Write file entry
175
- entry = Binary::LITStructures::LITFileEntry.new
176
- entry.filename_length = file_info[:lit_path].bytesize
177
- entry.filename = file_info[:lit_path]
178
- entry.offset = current_offset
179
- entry.compressed_size = compressed_size
180
- entry.uncompressed_size = file_info[:uncompressed_size]
181
- entry.flags = flags
182
-
183
- entry_data = entry.to_binary_s
184
- written = @io_system.write(output_handle, entry_data)
185
- total_bytes += written
186
-
187
- current_offset += compressed_size
188
- end
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 file_data [Array<Hash>] Array of file information with
197
- # compressed data
198
- # @return [Integer] Number of bytes written
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
- file_data.each do |file_info|
203
- written = @io_system.write(
204
- output_handle,
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
- total_bytes
211
- end
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
- # Calculate total header size (header + all file entries)
214
- #
215
- # @param file_data [Array<Hash>] Array of file information
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
- header_size
174
+ total_bytes
228
175
  end
229
176
 
230
- # Compress data using LZX
177
+ # Prepare file data for archiving
231
178
  #
232
- # @param data [String] Data to compress
233
- # @return [String] Compressed data
234
- def compress_data(data)
235
- input_handle = System::MemoryHandle.new(data)
236
- output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
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
- output_handle.data
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
- # Memory handles don't need closing but maintain consistency
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