cabriolet 0.2.0 → 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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +3 -0
  3. data/lib/cabriolet/binary/bitstream.rb +32 -21
  4. data/lib/cabriolet/binary/bitstream_writer.rb +21 -4
  5. data/lib/cabriolet/cab/compressor.rb +85 -53
  6. data/lib/cabriolet/cab/decompressor.rb +2 -1
  7. data/lib/cabriolet/cab/extractor.rb +2 -35
  8. data/lib/cabriolet/cab/file_compression_work.rb +52 -0
  9. data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
  10. data/lib/cabriolet/checksum.rb +49 -0
  11. data/lib/cabriolet/collections/file_collection.rb +175 -0
  12. data/lib/cabriolet/compressors/quantum.rb +3 -51
  13. data/lib/cabriolet/decompressors/quantum.rb +81 -52
  14. data/lib/cabriolet/extraction/base_extractor.rb +88 -0
  15. data/lib/cabriolet/extraction/extractor.rb +171 -0
  16. data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
  17. data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
  18. data/lib/cabriolet/format_base.rb +79 -0
  19. data/lib/cabriolet/hlp/quickhelp/compressor.rb +28 -503
  20. data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
  21. data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
  22. data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
  23. data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
  24. data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
  25. data/lib/cabriolet/huffman/encoder.rb +15 -12
  26. data/lib/cabriolet/lit/compressor.rb +45 -689
  27. data/lib/cabriolet/lit/content_encoder.rb +76 -0
  28. data/lib/cabriolet/lit/content_type_detector.rb +50 -0
  29. data/lib/cabriolet/lit/directory_builder.rb +153 -0
  30. data/lib/cabriolet/lit/guid_generator.rb +16 -0
  31. data/lib/cabriolet/lit/header_writer.rb +124 -0
  32. data/lib/cabriolet/lit/piece_builder.rb +74 -0
  33. data/lib/cabriolet/lit/structure_builder.rb +252 -0
  34. data/lib/cabriolet/quantum_shared.rb +105 -0
  35. data/lib/cabriolet/version.rb +1 -1
  36. data/lib/cabriolet.rb +114 -3
  37. metadata +38 -4
  38. data/lib/cabriolet/auto.rb +0 -173
  39. data/lib/cabriolet/parallel.rb +0 -333
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "topic_builder"
4
+ require_relative "topic_compressor"
5
+ require_relative "structure_builder"
6
+ require_relative "file_writer"
7
+
3
8
  module Cabriolet
4
9
  module HLP
5
10
  module QuickHelp
@@ -18,8 +23,7 @@ module Cabriolet
18
23
 
19
24
  # Initialize a new QuickHelp compressor
20
25
  #
21
- # @param io_system [System::IOSystem, nil] Custom I/O system or nil for
22
- # default
26
+ # @param io_system [System::IOSystem, nil] Custom I/O system or nil for default
23
27
  # @param algorithm_factory [AlgorithmFactory, nil] Custom algorithm factory or nil for default
24
28
  def initialize(io_system = nil, algorithm_factory = nil)
25
29
  @io_system = io_system || System::IOSystem.new
@@ -83,18 +87,20 @@ module Cabriolet
83
87
  topics = prepare_topics
84
88
 
85
89
  # Build QuickHelp structure
86
- qh_structure = build_quickhelp_structure(
87
- topics,
88
- version,
89
- database_name,
90
- control_char,
91
- case_sensitive,
90
+ structure_builder = StructureBuilder.new(
91
+ version: version,
92
+ database_name: database_name,
93
+ control_char: control_char,
94
+ case_sensitive: case_sensitive,
92
95
  )
96
+ qh_structure = structure_builder.build(topics)
93
97
 
94
98
  # Write to output file
95
99
  output_handle = @io_system.open(output_file, Constants::MODE_WRITE)
96
100
  begin
97
- bytes_written = write_quickhelp_file(output_handle, qh_structure)
101
+ file_writer = FileWriter.new(@io_system)
102
+ bytes_written = file_writer.write_quickhelp_file(output_handle,
103
+ qh_structure)
98
104
  bytes_written
99
105
  ensure
100
106
  @io_system.close(output_handle)
@@ -103,36 +109,21 @@ module Cabriolet
103
109
 
104
110
  private
105
111
 
106
- # Compress all files and collect metadata
107
- #
108
- # @return [Array<Hash>] Array of file information hashes
109
- def compress_all_files
110
- @files.map do |file_spec|
111
- compress_file_spec(file_spec)
112
- end
113
- end
114
-
115
- # Compress a single file specification
112
+ # Prepare topics from added files
116
113
  #
117
- # @param file_spec [Hash] File specification
118
- # @return [Hash] File information with compressed data
119
- def compress_file_spec(file_spec)
120
- # Get source data
121
- data = file_spec[:data] || read_file_data(file_spec[:source])
122
-
123
- # Compress if requested
124
- compressed_data = if file_spec[:compress]
125
- compress_data_lzss(data)
126
- else
127
- data
128
- end
114
+ # @return [Array<Hash>] Topic information
115
+ def prepare_topics
116
+ @files.map.with_index do |file_spec, index|
117
+ # Get source data
118
+ data = file_spec[:data] || read_file_data(file_spec[:source])
129
119
 
130
- {
131
- hlp_path: file_spec[:hlp_path],
132
- uncompressed_size: data.bytesize,
133
- compressed_data: compressed_data,
134
- compressed: file_spec[:compress],
135
- }
120
+ {
121
+ index: index,
122
+ text: data,
123
+ context: file_spec[:hlp_path],
124
+ compress: file_spec[:compress],
125
+ }
126
+ end
136
127
  end
137
128
 
138
129
  # Read file data from disk
@@ -154,472 +145,6 @@ module Cabriolet
154
145
  @io_system.close(handle)
155
146
  end
156
147
  end
157
-
158
- # Compress data using LZSS MODE_MSHELP
159
- #
160
- # @param data [String] Data to compress
161
- # @return [String] Compressed data
162
- def compress_data_lzss(data)
163
- input_handle = System::MemoryHandle.new(data)
164
- output_handle = System::MemoryHandle.new("", Constants::MODE_WRITE)
165
-
166
- compressor = @algorithm_factory.create(
167
- :lzss,
168
- :compressor,
169
- @io_system,
170
- input_handle,
171
- output_handle,
172
- DEFAULT_BUFFER_SIZE,
173
- mode: Compressors::LZSS::MODE_MSHELP,
174
- )
175
-
176
- compressor.compress
177
- output_handle.data
178
- end
179
-
180
- # Calculate directory size
181
- #
182
- # @param compressed_files [Array<Hash>] Compressed file information
183
- # @return [Integer] Directory size in bytes
184
- def calculate_directory_size(compressed_files)
185
- size = 0
186
- compressed_files.each do |file_info|
187
- # 4 bytes for filename length
188
- # N bytes for filename
189
- # 4 + 4 + 4 + 1 = 13 bytes for file metadata
190
- size += 4 + file_info[:hlp_path].bytesize + 13
191
- end
192
- size
193
- end
194
-
195
- # Write QuickHelp header
196
- #
197
- # @param output_handle [System::FileHandle] Output file handle
198
- # @param version [Integer] Format version
199
- # @param file_count [Integer] Number of files
200
- # @param directory_offset [Integer] Offset to directory
201
- # @return [Integer] Number of bytes written
202
- def write_header(output_handle, version, file_count, directory_offset)
203
- # NOTE: This is a simplified header format and does not match the actual QuickHelp FileHeader structure
204
- # TODO: Implement proper QuickHelp FileHeader usage
205
- header = Binary::HLPStructures::FileHeader.new
206
- header.signature = Binary::HLPStructures::SIGNATURE
207
- header.version = version
208
- header.file_count = file_count
209
- header.directory_offset = directory_offset
210
-
211
- header_data = header.to_binary_s
212
- written = @io_system.write(output_handle, header_data)
213
-
214
- unless written == header_data.bytesize
215
- raise Errors::CompressionError,
216
- "Failed to write QuickHelp header"
217
- end
218
-
219
- written
220
- end
221
-
222
- # Write file directory
223
- #
224
- # @param output_handle [System::FileHandle] Output file handle
225
- # @param compressed_files [Array<Hash>] Compressed file information
226
- # @return [Integer] Number of bytes written
227
- def write_directory(output_handle, compressed_files)
228
- bytes_written = 0
229
-
230
- compressed_files.each do |file_info|
231
- # Write filename length
232
- filename = file_info[:hlp_path].b
233
- length_data = [filename.bytesize].pack("V")
234
- bytes_written += @io_system.write(output_handle, length_data)
235
-
236
- # Write filename
237
- bytes_written += @io_system.write(output_handle, filename)
238
-
239
- # Write file metadata
240
- metadata = [
241
- file_info[:offset],
242
- file_info[:uncompressed_size],
243
- file_info[:compressed_data].bytesize,
244
- file_info[:compressed] ? 1 : 0,
245
- ].pack("V3C")
246
- bytes_written += @io_system.write(output_handle, metadata)
247
- end
248
-
249
- bytes_written
250
- end
251
-
252
- # Write file data
253
- #
254
- # @param output_handle [System::FileHandle] Output file handle
255
- # @param compressed_files [Array<Hash>] Compressed file information
256
- # @return [Integer] Number of bytes written
257
- def write_file_data(output_handle, compressed_files)
258
- bytes_written = 0
259
-
260
- compressed_files.each do |file_info|
261
- written = @io_system.write(
262
- output_handle,
263
- file_info[:compressed_data],
264
- )
265
- bytes_written += written
266
- end
267
-
268
- bytes_written
269
- end
270
-
271
- # Prepare topics from added files
272
- #
273
- # @return [Array<Hash>] Topic information
274
- def prepare_topics
275
- @files.map.with_index do |file_spec, index|
276
- # Get source data
277
- data = file_spec[:data] || read_file_data(file_spec[:source])
278
-
279
- {
280
- index: index,
281
- text: data,
282
- context: file_spec[:hlp_path],
283
- compress: file_spec[:compress],
284
- }
285
- end
286
- end
287
-
288
- # Build complete QuickHelp structure
289
- #
290
- # @param topics [Array<Hash>] Topic data
291
- # @param version [Integer] Format version
292
- # @param database_name [String] Database name
293
- # @param control_char [Integer] Control character
294
- # @param case_sensitive [Boolean] Case-sensitive contexts
295
- # @return [Hash] Complete structure ready for writing
296
- def build_quickhelp_structure(topics, version, database_name,
297
- control_char, case_sensitive)
298
- structure = {}
299
-
300
- # Compress topics
301
- structure[:topics] = compress_topics(topics)
302
-
303
- # Build context data
304
- structure[:contexts] = topics.map { |t| t[:context] }
305
- structure[:context_map] = topics.map.with_index { |_t, i| i }
306
-
307
- # Calculate offsets
308
- structure[:offsets] = calculate_offsets(structure)
309
-
310
- # Build header
311
- structure[:header] = build_header(
312
- structure,
313
- version,
314
- database_name,
315
- control_char,
316
- case_sensitive,
317
- )
318
-
319
- structure
320
- end
321
-
322
- # Compress all topics
323
- #
324
- # @param topics [Array<Hash>] Topic data
325
- # @return [Array<Hash>] Compressed topics
326
- def compress_topics(topics)
327
- topics.map do |topic|
328
- compressed = if topic[:compress]
329
- compress_topic_text(topic[:text])
330
- else
331
- topic[:text]
332
- end
333
-
334
- {
335
- text: topic[:text],
336
- compressed: compressed,
337
- decompressed_length: topic[:text].bytesize,
338
- compressed_length: compressed.bytesize,
339
- compress: topic[:compress],
340
- }
341
- end
342
- end
343
-
344
- # Compress topic text using QuickHelp keyword compression
345
- #
346
- # QuickHelp format uses:
347
- # - 0x00-0x0F: Literal bytes
348
- # - 0x10-0x17: Dictionary entry references
349
- # - 0x18: Run of spaces
350
- # - 0x19: Run of bytes (repeat)
351
- # - 0x1A: Escape byte (next byte is literal)
352
- # - 0x1B-0xFF: Literal bytes
353
- #
354
- # Without a dictionary, we encode literals directly and escape
355
- # control characters (0x10-0x1A) with 0x1A prefix.
356
- #
357
- # Topic text format:
358
- # - Each line: [len][text][newline][attr_len][attrs][0xFF]
359
- # - len includes itself, text, newline, attr_len
360
- # - attr_len includes itself and attrs, minimum 1 (just 0xFF terminator)
361
- #
362
- # @param text [String] Topic text
363
- # @return [String] Compressed data with length header
364
- def compress_topic_text(text)
365
- # Build topic in QuickHelp internal format
366
- topic_data = build_topic_data(text)
367
-
368
- # Encode using QuickHelp keyword compression format
369
- encoded = encode_keyword_compression(topic_data)
370
-
371
- # Prepend decompressed length (2 bytes)
372
- length_header = [topic_data.bytesize].pack("v")
373
- length_header + encoded
374
- end
375
-
376
- # Build topic data in QuickHelp internal format
377
- #
378
- # Topic format as expected by decompressor:
379
- # - Each line: [text_length][text][newline][attr_len][attrs][0xFF]
380
- # - text_length = text + newline + 1 (for attr_len byte) = text_bytes + 2
381
- # - Line structure: text_length byte + text + newline + attr_len byte + attr_data
382
- #
383
- # The decompressor reads:
384
- # - text_length = data.getbyte(pos)
385
- # - text_bytes = text_length - 2 (reads text, skips newline)
386
- # - attr_length = data.getbyte(pos after text + newline)
387
- #
388
- # @param text [String] Raw topic text
389
- # @return [String] Formatted topic data
390
- def build_topic_data(text)
391
- result = +""
392
-
393
- # Split text into lines
394
- lines = text.split("\n")
395
-
396
- lines.each do |line|
397
- text_bytes = line.b
398
- newline = "\x0D" # Carriage return
399
-
400
- # Attribute section: just 0xFF terminator (attr_len = 1)
401
- attr_data = "\xFF"
402
- attr_len = 1
403
-
404
- # text_length = text + newline + 1 (for attr_len byte)
405
- # This ensures text_bytes = text_length - 2 gives correct text length
406
- text_length = text_bytes.bytesize + 2
407
-
408
- result << text_length.chr
409
- result << text_bytes
410
- result << newline
411
- result << attr_len.chr
412
- result << attr_data
413
- end
414
-
415
- result
416
- end
417
-
418
- # Encode data using QuickHelp keyword compression format
419
- #
420
- # @param data [String] Data to encode
421
- # @return [String] Encoded data
422
- def encode_keyword_compression(data)
423
- result = +""
424
-
425
- data.bytes.each do |byte|
426
- if byte < 0x10 || byte == 0x1B || byte > 0x1A
427
- # Literal byte (except control range 0x10-0x1A)
428
- result << byte.chr
429
- elsif byte.between?(0x10, 0x1A)
430
- # Control byte - escape it
431
- result << 0x1A.chr << byte.chr
432
- else
433
- # 0x1B is also literal (above control range)
434
- result << byte.chr
435
- end
436
- end
437
-
438
- result
439
- end
440
-
441
- # Calculate all offsets in the file
442
- #
443
- # @param structure [Hash] QuickHelp structure
444
- # @return [Hash] Calculated offsets
445
- def calculate_offsets(structure)
446
- offsets = {}
447
-
448
- # Start after file header (70 bytes = 2 signature + 68 header)
449
- current_offset = 70
450
-
451
- # Topic index: (topic_count + 1) * 4 bytes
452
- offsets[:topic_index] = current_offset
453
- topic_count = structure[:topics].size
454
- current_offset += (topic_count + 1) * 4
455
-
456
- # Context strings: sum of string lengths + null terminators
457
- offsets[:context_strings] = current_offset
458
- structure[:contexts].each do |ctx|
459
- current_offset += ctx.bytesize + 1 # +1 for null terminator
460
- end
461
-
462
- # Context map: context_count * 2 bytes
463
- offsets[:context_map] = current_offset
464
- current_offset += structure[:context_map].size * 2
465
-
466
- # Keywords: not implemented yet, set to 0
467
- offsets[:keywords] = 0
468
-
469
- # Huffman tree: not implemented yet, set to 0
470
- offsets[:huffman_tree] = 0
471
-
472
- # Topic text: starts after context map
473
- offsets[:topic_text] = current_offset
474
-
475
- # Calculate topic text offsets
476
- offsets[:topic_offsets] = []
477
- structure[:topics].each do |topic|
478
- offsets[:topic_offsets] << (current_offset - offsets[:topic_text])
479
- current_offset += topic[:compressed_length]
480
- end
481
- # Add end marker
482
- offsets[:topic_offsets] << (current_offset - offsets[:topic_text])
483
-
484
- # Total database size
485
- offsets[:database_size] = current_offset
486
-
487
- offsets
488
- end
489
-
490
- # Build file header
491
- #
492
- # @param structure [Hash] QuickHelp structure
493
- # @param version [Integer] Format version
494
- # @param database_name [String] Database name
495
- # @param control_char [Integer] Control character
496
- # @param case_sensitive [Boolean] Case-sensitive contexts
497
- # @return [Hash] Header information
498
- def build_header(structure, version, database_name, control_char,
499
- case_sensitive)
500
- attributes = 0
501
- attributes |= Binary::HLPStructures::Attributes::CASE_SENSITIVE if case_sensitive
502
-
503
- {
504
- version: version,
505
- attributes: attributes,
506
- control_character: control_char,
507
- topic_count: structure[:topics].size,
508
- context_count: structure[:contexts].size,
509
- display_width: 80,
510
- predefined_ctx_count: 0,
511
- database_name: database_name.ljust(14, "\x00")[0, 14],
512
- offsets: structure[:offsets],
513
- }
514
- end
515
-
516
- # Write complete QuickHelp file
517
- #
518
- # @param output_handle [System::FileHandle] Output file handle
519
- # @param structure [Hash] QuickHelp structure
520
- # @return [Integer] Bytes written
521
- def write_quickhelp_file(output_handle, structure)
522
- bytes_written = 0
523
-
524
- # Write file header
525
- bytes_written += write_file_header(output_handle, structure[:header])
526
-
527
- # Write topic index
528
- bytes_written += write_topic_index(output_handle,
529
- structure[:header][:offsets])
530
-
531
- # Write context strings
532
- bytes_written += write_context_strings(output_handle,
533
- structure[:contexts])
534
-
535
- # Write context map
536
- bytes_written += write_context_map(output_handle,
537
- structure[:context_map])
538
-
539
- # Write topic texts
540
- bytes_written += write_topic_texts(output_handle, structure[:topics])
541
-
542
- bytes_written
543
- end
544
-
545
- # Write file header
546
- #
547
- # @param output_handle [System::FileHandle] Output file handle
548
- # @param header_info [Hash] Header information
549
- # @return [Integer] Bytes written
550
- def write_file_header(output_handle, header_info)
551
- header = Binary::HLPStructures::FileHeader.new
552
- header.signature = Binary::HLPStructures::SIGNATURE
553
- header.version = header_info[:version]
554
- header.attributes = header_info[:attributes]
555
- header.control_character = header_info[:control_character]
556
- header.padding1 = 0
557
- header.topic_count = header_info[:topic_count]
558
- header.context_count = header_info[:context_count]
559
- header.display_width = header_info[:display_width]
560
- header.padding2 = 0
561
- header.predefined_ctx_count = header_info[:predefined_ctx_count]
562
- header.database_name = header_info[:database_name]
563
- header.reserved1 = 0
564
- header.topic_index_offset = header_info[:offsets][:topic_index]
565
- header.context_strings_offset = header_info[:offsets][:context_strings]
566
- header.context_map_offset = header_info[:offsets][:context_map]
567
- header.keywords_offset = header_info[:offsets][:keywords]
568
- header.huffman_tree_offset = header_info[:offsets][:huffman_tree]
569
- header.topic_text_offset = header_info[:offsets][:topic_text]
570
- header.reserved2 = 0
571
- header.reserved3 = 0
572
- header.database_size = header_info[:offsets][:database_size]
573
-
574
- header_data = header.to_binary_s
575
- @io_system.write(output_handle, header_data)
576
- end
577
-
578
- # Write topic index
579
- #
580
- # @param output_handle [System::FileHandle] Output file handle
581
- # @param offsets [Hash] Offset information
582
- # @return [Integer] Bytes written
583
- def write_topic_index(output_handle, offsets)
584
- # Write all topic offsets including end marker
585
- index_data = offsets[:topic_offsets].pack("V*")
586
- @io_system.write(output_handle, index_data)
587
- end
588
-
589
- # Write context strings
590
- #
591
- # @param output_handle [System::FileHandle] Output file handle
592
- # @param contexts [Array<String>] Context strings
593
- # @return [Integer] Bytes written
594
- def write_context_strings(output_handle, contexts)
595
- data = contexts.map { |ctx| "#{ctx}\u0000" }.join
596
- @io_system.write(output_handle, data)
597
- end
598
-
599
- # Write context map
600
- #
601
- # @param output_handle [System::FileHandle] Output file handle
602
- # @param context_map [Array<Integer>] Topic indices
603
- # @return [Integer] Bytes written
604
- def write_context_map(output_handle, context_map)
605
- map_data = context_map.pack("v*")
606
- @io_system.write(output_handle, map_data)
607
- end
608
-
609
- # Write topic texts
610
- #
611
- # @param output_handle [System::FileHandle] Output file handle
612
- # @param topics [Array<Hash>] Compressed topics
613
- # @return [Integer] Bytes written
614
- def write_topic_texts(output_handle, topics)
615
- total_bytes = 0
616
-
617
- topics.each do |topic|
618
- total_bytes += @io_system.write(output_handle, topic[:compressed])
619
- end
620
-
621
- total_bytes
622
- end
623
148
  end
624
149
  end
625
150
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ module HLP
5
+ module QuickHelp
6
+ # Writes QuickHelp files to disk
7
+ class FileWriter
8
+ # Initialize file writer
9
+ #
10
+ # @param io_system [System::IOSystem] I/O system for writing
11
+ def initialize(io_system)
12
+ @io_system = io_system
13
+ end
14
+
15
+ # Write complete QuickHelp file
16
+ #
17
+ # @param output_handle [System::FileHandle] Output file handle
18
+ # @param structure [Hash] QuickHelp structure
19
+ # @return [Integer] Bytes written
20
+ def write_quickhelp_file(output_handle, structure)
21
+ bytes_written = 0
22
+
23
+ # Write file header
24
+ bytes_written += write_file_header(output_handle, structure[:header])
25
+
26
+ # Write topic index
27
+ bytes_written += write_topic_index(output_handle,
28
+ structure[:header][:offsets])
29
+
30
+ # Write context strings
31
+ bytes_written += write_context_strings(output_handle,
32
+ structure[:contexts])
33
+
34
+ # Write context map
35
+ bytes_written += write_context_map(output_handle,
36
+ structure[:context_map])
37
+
38
+ # Write topic texts
39
+ bytes_written += write_topic_texts(output_handle, structure[:topics])
40
+
41
+ bytes_written
42
+ end
43
+
44
+ # Write file header
45
+ #
46
+ # @param output_handle [System::FileHandle] Output file handle
47
+ # @param header_info [Hash] Header information
48
+ # @return [Integer] Bytes written
49
+ def write_file_header(output_handle, header_info)
50
+ header = Binary::HLPStructures::FileHeader.new
51
+ header.signature = Binary::HLPStructures::SIGNATURE
52
+ header.version = header_info[:version]
53
+ header.attributes = header_info[:attributes]
54
+ header.control_character = header_info[:control_character]
55
+ header.padding1 = 0
56
+ header.topic_count = header_info[:topic_count]
57
+ header.context_count = header_info[:context_count]
58
+ header.display_width = header_info[:display_width]
59
+ header.padding2 = 0
60
+ header.predefined_ctx_count = header_info[:predefined_ctx_count]
61
+ header.database_name = header_info[:database_name]
62
+ header.reserved1 = 0
63
+ header.topic_index_offset = header_info[:offsets][:topic_index]
64
+ header.context_strings_offset = header_info[:offsets][:context_strings]
65
+ header.context_map_offset = header_info[:offsets][:context_map]
66
+ header.keywords_offset = header_info[:offsets][:keywords]
67
+ header.huffman_tree_offset = header_info[:offsets][:huffman_tree]
68
+ header.topic_text_offset = header_info[:offsets][:topic_text]
69
+ header.reserved2 = 0
70
+ header.reserved3 = 0
71
+ header.database_size = header_info[:offsets][:database_size]
72
+
73
+ header_data = header.to_binary_s
74
+ @io_system.write(output_handle, header_data)
75
+ end
76
+
77
+ # Write topic index
78
+ #
79
+ # @param output_handle [System::FileHandle] Output file handle
80
+ # @param offsets [Hash] Offset information
81
+ # @return [Integer] Bytes written
82
+ def write_topic_index(output_handle, offsets)
83
+ # Write all topic offsets including end marker
84
+ index_data = offsets[:topic_offsets].pack("V*")
85
+ @io_system.write(output_handle, index_data)
86
+ end
87
+
88
+ # Write context strings
89
+ #
90
+ # @param output_handle [System::FileHandle] Output file handle
91
+ # @param contexts [Array<String>] Context strings
92
+ # @return [Integer] Bytes written
93
+ def write_context_strings(output_handle, contexts)
94
+ data = contexts.map { |ctx| "#{ctx}\u0000" }.join
95
+ @io_system.write(output_handle, data)
96
+ end
97
+
98
+ # Write context map
99
+ #
100
+ # @param output_handle [System::FileHandle] Output file handle
101
+ # @param context_map [Array<Integer>] Topic indices
102
+ # @return [Integer] Bytes written
103
+ def write_context_map(output_handle, context_map)
104
+ map_data = context_map.pack("v*")
105
+ @io_system.write(output_handle, map_data)
106
+ end
107
+
108
+ # Write topic texts
109
+ #
110
+ # @param output_handle [System::FileHandle] Output file handle
111
+ # @param topics [Array<Hash>] Compressed topics
112
+ # @return [Integer] Bytes written
113
+ def write_topic_texts(output_handle, topics)
114
+ total_bytes = 0
115
+
116
+ topics.each do |topic|
117
+ total_bytes += @io_system.write(output_handle, topic[:compressed])
118
+ end
119
+
120
+ total_bytes
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end