cabriolet 0.1.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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +799 -0
  3. data/CHANGELOG.md +44 -0
  4. data/LICENSE +29 -0
  5. data/README.adoc +1207 -0
  6. data/exe/cabriolet +6 -0
  7. data/lib/cabriolet/auto.rb +173 -0
  8. data/lib/cabriolet/binary/bitstream.rb +148 -0
  9. data/lib/cabriolet/binary/bitstream_writer.rb +180 -0
  10. data/lib/cabriolet/binary/chm_structures.rb +213 -0
  11. data/lib/cabriolet/binary/hlp_structures.rb +66 -0
  12. data/lib/cabriolet/binary/kwaj_structures.rb +74 -0
  13. data/lib/cabriolet/binary/lit_structures.rb +107 -0
  14. data/lib/cabriolet/binary/oab_structures.rb +112 -0
  15. data/lib/cabriolet/binary/structures.rb +56 -0
  16. data/lib/cabriolet/binary/szdd_structures.rb +60 -0
  17. data/lib/cabriolet/cab/compressor.rb +382 -0
  18. data/lib/cabriolet/cab/decompressor.rb +510 -0
  19. data/lib/cabriolet/cab/extractor.rb +357 -0
  20. data/lib/cabriolet/cab/parser.rb +264 -0
  21. data/lib/cabriolet/chm/compressor.rb +513 -0
  22. data/lib/cabriolet/chm/decompressor.rb +436 -0
  23. data/lib/cabriolet/chm/parser.rb +254 -0
  24. data/lib/cabriolet/cli.rb +776 -0
  25. data/lib/cabriolet/compressors/base.rb +34 -0
  26. data/lib/cabriolet/compressors/lzss.rb +250 -0
  27. data/lib/cabriolet/compressors/lzx.rb +581 -0
  28. data/lib/cabriolet/compressors/mszip.rb +315 -0
  29. data/lib/cabriolet/compressors/quantum.rb +446 -0
  30. data/lib/cabriolet/constants.rb +75 -0
  31. data/lib/cabriolet/decompressors/base.rb +39 -0
  32. data/lib/cabriolet/decompressors/lzss.rb +138 -0
  33. data/lib/cabriolet/decompressors/lzx.rb +726 -0
  34. data/lib/cabriolet/decompressors/mszip.rb +390 -0
  35. data/lib/cabriolet/decompressors/none.rb +27 -0
  36. data/lib/cabriolet/decompressors/quantum.rb +456 -0
  37. data/lib/cabriolet/errors.rb +39 -0
  38. data/lib/cabriolet/format_detector.rb +156 -0
  39. data/lib/cabriolet/hlp/compressor.rb +272 -0
  40. data/lib/cabriolet/hlp/decompressor.rb +198 -0
  41. data/lib/cabriolet/hlp/parser.rb +131 -0
  42. data/lib/cabriolet/huffman/decoder.rb +79 -0
  43. data/lib/cabriolet/huffman/encoder.rb +108 -0
  44. data/lib/cabriolet/huffman/tree.rb +138 -0
  45. data/lib/cabriolet/kwaj/compressor.rb +479 -0
  46. data/lib/cabriolet/kwaj/decompressor.rb +237 -0
  47. data/lib/cabriolet/kwaj/parser.rb +183 -0
  48. data/lib/cabriolet/lit/compressor.rb +255 -0
  49. data/lib/cabriolet/lit/decompressor.rb +250 -0
  50. data/lib/cabriolet/models/cabinet.rb +81 -0
  51. data/lib/cabriolet/models/chm_file.rb +28 -0
  52. data/lib/cabriolet/models/chm_header.rb +67 -0
  53. data/lib/cabriolet/models/chm_section.rb +38 -0
  54. data/lib/cabriolet/models/file.rb +119 -0
  55. data/lib/cabriolet/models/folder.rb +102 -0
  56. data/lib/cabriolet/models/folder_data.rb +21 -0
  57. data/lib/cabriolet/models/hlp_file.rb +45 -0
  58. data/lib/cabriolet/models/hlp_header.rb +37 -0
  59. data/lib/cabriolet/models/kwaj_header.rb +98 -0
  60. data/lib/cabriolet/models/lit_header.rb +55 -0
  61. data/lib/cabriolet/models/oab_header.rb +95 -0
  62. data/lib/cabriolet/models/szdd_header.rb +72 -0
  63. data/lib/cabriolet/modifier.rb +326 -0
  64. data/lib/cabriolet/oab/compressor.rb +353 -0
  65. data/lib/cabriolet/oab/decompressor.rb +315 -0
  66. data/lib/cabriolet/parallel.rb +333 -0
  67. data/lib/cabriolet/repairer.rb +288 -0
  68. data/lib/cabriolet/streaming.rb +221 -0
  69. data/lib/cabriolet/system/file_handle.rb +107 -0
  70. data/lib/cabriolet/system/io_system.rb +87 -0
  71. data/lib/cabriolet/system/memory_handle.rb +105 -0
  72. data/lib/cabriolet/szdd/compressor.rb +217 -0
  73. data/lib/cabriolet/szdd/decompressor.rb +184 -0
  74. data/lib/cabriolet/szdd/parser.rb +127 -0
  75. data/lib/cabriolet/validator.rb +332 -0
  76. data/lib/cabriolet/version.rb +5 -0
  77. data/lib/cabriolet.rb +104 -0
  78. metadata +157 -0
data/exe/cabriolet ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/cabriolet"
5
+
6
+ Cabriolet::CLI.start(ARGV)
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "format_detector"
4
+
5
+ module Cabriolet
6
+ # Auto-detection and extraction module
7
+ module Auto
8
+ class << self
9
+ # Open and parse an archive with automatic format detection
10
+ #
11
+ # @param path [String] Path to the archive file
12
+ # @param options [Hash] Options to pass to the parser
13
+ # @return [Object] Parsed archive object
14
+ # @raise [UnsupportedFormatError] if format cannot be detected or is unsupported
15
+ #
16
+ # @example
17
+ # archive = Cabriolet::Auto.open('unknown.archive')
18
+ # archive.files.each { |f| puts f.name }
19
+ def open(path, **options)
20
+ format = FormatDetector.detect(path)
21
+ unless format
22
+ raise UnsupportedFormatError,
23
+ "Unable to detect format for: #{path}"
24
+ end
25
+
26
+ parser_class = FormatDetector.format_to_parser(format)
27
+ unless parser_class
28
+ raise UnsupportedFormatError,
29
+ "No parser available for format: #{format}"
30
+ end
31
+
32
+ parser_class.new(**options).parse(path)
33
+ end
34
+
35
+ # Detect format and extract all files automatically
36
+ #
37
+ # @param archive_path [String] Path to the archive
38
+ # @param output_dir [String] Directory to extract to
39
+ # @param options [Hash] Extraction options
40
+ # @option options [Boolean] :preserve_paths (true) Preserve directory structure
41
+ # @option options [Boolean] :overwrite (false) Overwrite existing files
42
+ # @option options [Boolean] :parallel (false) Use parallel extraction
43
+ # @option options [Integer] :workers (4) Number of parallel workers
44
+ # @return [Hash] Extraction statistics
45
+ #
46
+ # @example
47
+ # Cabriolet::Auto.extract('archive.cab', 'output/')
48
+ # Cabriolet::Auto.extract('file.chm', 'docs/', parallel: true, workers: 8)
49
+ def extract(archive_path, output_dir, **options)
50
+ archive = open(archive_path)
51
+
52
+ extractor = if options[:parallel]
53
+ ParallelExtractor.new(archive, output_dir, **options)
54
+ else
55
+ SimpleExtractor.new(archive, output_dir, **options)
56
+ end
57
+
58
+ extractor.extract_all
59
+ end
60
+
61
+ # Detect format only without parsing
62
+ #
63
+ # @param path [String] Path to the file
64
+ # @return [Symbol, nil] Detected format symbol or nil
65
+ #
66
+ # @example
67
+ # format = Cabriolet::Auto.detect_format('file.cab')
68
+ # # => :cab
69
+ def detect_format(path)
70
+ FormatDetector.detect(path)
71
+ end
72
+
73
+ # Get information about an archive without full extraction
74
+ #
75
+ # @param path [String] Path to the archive
76
+ # @return [Hash] Archive information
77
+ #
78
+ # @example
79
+ # info = Cabriolet::Auto.info('archive.cab')
80
+ # # => { format: :cab, file_count: 145, total_size: 52428800, ... }
81
+ def info(path)
82
+ archive = open(path)
83
+ format = detect_format(path)
84
+
85
+ {
86
+ format: format,
87
+ path: path,
88
+ file_count: archive.files.count,
89
+ total_size: archive.files.sum { |f| f.size || 0 },
90
+ compressed_size: File.size(path),
91
+ compression_ratio: calculate_compression_ratio(archive, path),
92
+ files: archive.files.map { |f| file_info(f) },
93
+ }
94
+ end
95
+
96
+ private
97
+
98
+ def calculate_compression_ratio(archive, path)
99
+ total_uncompressed = archive.files.sum { |f| f.size || 0 }
100
+ compressed = File.size(path)
101
+
102
+ return 0 if total_uncompressed.zero?
103
+
104
+ ((compressed.to_f / total_uncompressed) * 100).round(2)
105
+ end
106
+
107
+ def file_info(file)
108
+ {
109
+ name: file.name,
110
+ size: file.size,
111
+ compressed_size: file.respond_to?(:compressed_size) ? file.compressed_size : nil,
112
+ attributes: file.respond_to?(:attributes) ? file.attributes : nil,
113
+ date: file.respond_to?(:date) ? file.date : nil,
114
+ time: file.respond_to?(:time) ? file.time : nil,
115
+ }
116
+ end
117
+ end
118
+
119
+ # Simple sequential extractor
120
+ class SimpleExtractor
121
+ def initialize(archive, output_dir, **options)
122
+ @archive = archive
123
+ @output_dir = output_dir
124
+ @options = options
125
+ @preserve_paths = options.fetch(:preserve_paths, true)
126
+ @overwrite = options.fetch(:overwrite, false)
127
+ @stats = { extracted: 0, skipped: 0, failed: 0, bytes: 0 }
128
+ end
129
+
130
+ def extract_all
131
+ FileUtils.mkdir_p(@output_dir)
132
+
133
+ @archive.files.each do |file|
134
+ extract_file(file)
135
+ end
136
+
137
+ @stats
138
+ end
139
+
140
+ private
141
+
142
+ def extract_file(file)
143
+ output_path = build_output_path(file.name)
144
+
145
+ if File.exist?(output_path) && !@overwrite
146
+ @stats[:skipped] += 1
147
+ return
148
+ end
149
+
150
+ FileUtils.mkdir_p(File.dirname(output_path))
151
+ File.write(output_path, file.data, mode: "wb")
152
+
153
+ @stats[:extracted] += 1
154
+ @stats[:bytes] += file.data.bytesize
155
+ rescue StandardError => e
156
+ @stats[:failed] += 1
157
+ warn "Failed to extract #{file.name}: #{e.message}"
158
+ end
159
+
160
+ def build_output_path(filename)
161
+ if @preserve_paths
162
+ # Keep directory structure
163
+ clean_name = filename.gsub("\\", "/")
164
+ File.join(@output_dir, clean_name)
165
+ else
166
+ # Flatten to output directory
167
+ base_name = File.basename(filename.gsub("\\", "/"))
168
+ File.join(@output_dir, base_name)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ module Binary
5
+ # Bitstream provides bit-level I/O operations for reading compressed data
6
+ class Bitstream
7
+ attr_reader :io_system, :handle, :buffer_size
8
+
9
+ # Initialize a new bitstream
10
+ #
11
+ # @param io_system [System::IOSystem] I/O system for reading data
12
+ # @param handle [System::FileHandle, System::MemoryHandle] Handle to read from
13
+ # @param buffer_size [Integer] Size of the input buffer
14
+ def initialize(io_system, handle,
15
+ buffer_size = Cabriolet.default_buffer_size)
16
+ @io_system = io_system
17
+ @handle = handle
18
+ @buffer_size = buffer_size
19
+ @buffer = ""
20
+ @buffer_pos = 0
21
+ @bit_buffer = 0
22
+ @bits_left = 0
23
+ end
24
+
25
+ # Read specified number of bits from the stream
26
+ #
27
+ # @param num_bits [Integer] Number of bits to read (1-32)
28
+ # @return [Integer] Bits read as an integer
29
+ # @raise [DecompressionError] if unable to read required bits
30
+ def read_bits(num_bits)
31
+ if num_bits < 1 || num_bits > 32
32
+ raise ArgumentError,
33
+ "Can only read 1-32 bits at a time"
34
+ end
35
+
36
+ # Ensure we have enough bits in the buffer
37
+ while @bits_left < num_bits
38
+ byte = read_byte
39
+ return 0 if byte.nil? # EOF
40
+
41
+ @bit_buffer |= (byte << @bits_left)
42
+ @bits_left += 8
43
+ end
44
+
45
+ # Extract the requested bits
46
+ result = @bit_buffer & ((1 << num_bits) - 1)
47
+ @bit_buffer >>= num_bits
48
+ @bits_left -= num_bits
49
+
50
+ result
51
+ end
52
+
53
+ # Read a single byte from the input
54
+ #
55
+ # @return [Integer, nil] Byte value or nil at EOF
56
+ def read_byte
57
+ if @buffer_pos >= @buffer.bytesize
58
+ @buffer = @io_system.read(@handle, @buffer_size)
59
+ @buffer_pos = 0
60
+ return nil if @buffer.empty?
61
+ end
62
+
63
+ byte = @buffer.getbyte(@buffer_pos)
64
+ @buffer_pos += 1
65
+ byte
66
+ end
67
+
68
+ # Align to the next byte boundary
69
+ #
70
+ # @return [void]
71
+ def byte_align
72
+ discard_bits = @bits_left % 8
73
+ @bit_buffer >>= discard_bits
74
+ @bits_left -= discard_bits
75
+ end
76
+
77
+ # Peek at bits without consuming them
78
+ #
79
+ # @param num_bits [Integer] Number of bits to peek at
80
+ # @return [Integer] Bits as an integer
81
+ def peek_bits(num_bits)
82
+ if num_bits < 1 || num_bits > 32
83
+ raise ArgumentError,
84
+ "Can only peek 1-32 bits at a time"
85
+ end
86
+
87
+ # Ensure we have enough bits
88
+ while @bits_left < num_bits
89
+ byte = read_byte
90
+ return 0 if byte.nil?
91
+
92
+ @bit_buffer |= (byte << @bits_left)
93
+ @bits_left += 8
94
+ end
95
+
96
+ @bit_buffer & ((1 << num_bits) - 1)
97
+ end
98
+
99
+ # Skip specified number of bits
100
+ #
101
+ # @param num_bits [Integer] Number of bits to skip
102
+ # @return [void]
103
+ def skip_bits(num_bits)
104
+ read_bits(num_bits)
105
+ nil
106
+ end
107
+
108
+ # Read bits in big-endian (MSB first) order
109
+ #
110
+ # @param num_bits [Integer] Number of bits to read
111
+ # @return [Integer] Bits as an integer
112
+ def read_bits_be(num_bits)
113
+ result = 0
114
+ num_bits.times do
115
+ result = (result << 1) | read_bits(1)
116
+ end
117
+ result
118
+ end
119
+
120
+ # Read a 16-bit little-endian value
121
+ #
122
+ # @return [Integer] 16-bit value
123
+ def read_uint16_le
124
+ read_bits(16)
125
+ end
126
+
127
+ # Read a 32-bit little-endian value
128
+ #
129
+ # @return [Integer] 32-bit value
130
+ def read_uint32_le
131
+ low = read_bits(16)
132
+ high = read_bits(16)
133
+ (high << 16) | low
134
+ end
135
+
136
+ # Reset the bitstream state
137
+ #
138
+ # @return [void]
139
+ def reset
140
+ @buffer = ""
141
+ @buffer_pos = 0
142
+ @bit_buffer = 0
143
+ @bits_left = 0
144
+ @io_system.seek(@handle, 0, Constants::SEEK_START)
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cabriolet
4
+ module Binary
5
+ # BitstreamWriter provides bit-level I/O operations for writing compressed data
6
+ class BitstreamWriter
7
+ attr_reader :io_system, :handle, :buffer_size
8
+
9
+ # Initialize a new bitstream writer
10
+ #
11
+ # @param io_system [System::IOSystem] I/O system for writing data
12
+ # @param handle [System::FileHandle, System::MemoryHandle] Handle to write to
13
+ # @param buffer_size [Integer] Size of the output buffer
14
+ # @param msb_first [Boolean] Whether to write bits MSB-first (for Quantum)
15
+ def initialize(io_system, handle,
16
+ buffer_size = Cabriolet.default_buffer_size, msb_first: false)
17
+ @io_system = io_system
18
+ @handle = handle
19
+ @buffer_size = buffer_size
20
+ @msb_first = msb_first
21
+ @bit_buffer = 0
22
+ @bits_in_buffer = 0
23
+ end
24
+
25
+ # Write specified number of bits to the stream
26
+ #
27
+ # @param value [Integer] Value to write
28
+ # @param num_bits [Integer] Number of bits to write (1-32)
29
+ # @return [void]
30
+ # @raise [ArgumentError] if num_bits is out of range
31
+ def write_bits(value, num_bits)
32
+ if num_bits < 1 || num_bits > 32
33
+ raise ArgumentError,
34
+ "Can only write 1-32 bits at a time"
35
+ end
36
+
37
+ # Add bits to buffer (LSB first, like DEFLATE)
38
+ @bit_buffer |= ((value & ((1 << num_bits) - 1)) << @bits_in_buffer)
39
+ @bits_in_buffer += num_bits
40
+
41
+ # Flush complete bytes
42
+ while @bits_in_buffer >= 8
43
+ byte = @bit_buffer & 0xFF
44
+ write_byte(byte)
45
+ @bit_buffer >>= 8
46
+ @bits_in_buffer -= 8
47
+ end
48
+ end
49
+
50
+ # Align to the next byte boundary by padding with zeros
51
+ #
52
+ # @return [void]
53
+ def byte_align
54
+ return if @bits_in_buffer.zero?
55
+
56
+ # Pad with zeros to complete the current byte
57
+ padding_bits = 8 - (@bits_in_buffer % 8)
58
+ write_bits(0, padding_bits) if padding_bits < 8
59
+ end
60
+
61
+ # Flush any remaining bits in the buffer
62
+ #
63
+ # @return [void]
64
+ def flush
65
+ return if @bits_in_buffer.zero?
66
+
67
+ # Write any remaining bits (padded with zeros)
68
+ byte = @bit_buffer & 0xFF
69
+ write_byte(byte)
70
+ @bit_buffer = 0
71
+ @bits_in_buffer = 0
72
+ end
73
+
74
+ # Write a single byte to the output
75
+ #
76
+ # @param byte [Integer] Byte value to write
77
+ # @return [void]
78
+ def write_byte(byte)
79
+ data = [byte].pack("C")
80
+ @io_system.write(@handle, data)
81
+ end
82
+
83
+ # Write a raw byte directly (for signatures, etc.)
84
+ # This ensures the bit buffer is flushed first
85
+ #
86
+ # @param byte [Integer] Byte value to write
87
+ # @return [void]
88
+ def write_raw_byte(byte)
89
+ flush if @bits_in_buffer.positive?
90
+ write_byte(byte)
91
+ end
92
+
93
+ # Write multiple bytes to the output
94
+ #
95
+ # @param bytes [String, Array<Integer>] Bytes to write
96
+ # @return [void]
97
+ def write_bytes(bytes)
98
+ data = bytes.is_a?(String) ? bytes : bytes.pack("C*")
99
+ @io_system.write(@handle, data)
100
+ end
101
+
102
+ # Write bits in big-endian (MSB first) order
103
+ #
104
+ # @param value [Integer] Value to write
105
+ # @param num_bits [Integer] Number of bits to write
106
+ # @return [void]
107
+ def write_bits_be(value, num_bits)
108
+ num_bits.times do |i|
109
+ bit = (value >> (num_bits - 1 - i)) & 1
110
+ write_bits(bit, 1)
111
+ end
112
+ end
113
+
114
+ # Write a 16-bit little-endian value
115
+ #
116
+ # @param value [Integer] 16-bit value
117
+ # @return [void]
118
+ def write_uint16_le(value)
119
+ write_bits(value & 0xFFFF, 16)
120
+ end
121
+
122
+ # Write a 32-bit little-endian value
123
+ #
124
+ # @param value [Integer] 32-bit value
125
+ # @return [void]
126
+ def write_uint32_le(value)
127
+ write_bits(value & 0xFFFF, 16)
128
+ write_bits((value >> 16) & 0xFFFF, 16)
129
+ end
130
+
131
+ # Write bits MSB-first (for Quantum compression)
132
+ # Accumulates bits and writes 16-bit words MSB-first
133
+ #
134
+ # @param value [Integer] Value to write
135
+ # @param num_bits [Integer] Number of bits to write
136
+ # @return [void]
137
+ def write_bits_msb(value, num_bits)
138
+ if num_bits < 1 || num_bits > 32
139
+ raise ArgumentError,
140
+ "Can only write 1-32 bits at a time"
141
+ end
142
+
143
+ # Add bits to buffer (MSB first)
144
+ @bit_buffer = (@bit_buffer << num_bits) | (value & ((1 << num_bits) - 1))
145
+ @bits_in_buffer += num_bits
146
+
147
+ # Flush complete 16-bit words MSB-first
148
+ while @bits_in_buffer >= 16
149
+ @bits_in_buffer -= 16
150
+ word = (@bit_buffer >> @bits_in_buffer) & 0xFFFF
151
+ # Write MSB first
152
+ write_byte((word >> 8) & 0xFF)
153
+ write_byte(word & 0xFF)
154
+ end
155
+ end
156
+
157
+ # Flush MSB buffer (write remaining bits padded to 16-bit boundary)
158
+ #
159
+ # @return [void]
160
+ def flush_msb
161
+ return if @bits_in_buffer.zero?
162
+
163
+ # Pad to 16-bit boundary
164
+ padding = (16 - @bits_in_buffer) % 16
165
+ @bit_buffer <<= padding if padding.positive?
166
+ @bits_in_buffer += padding
167
+
168
+ # Write final 16-bit word
169
+ if @bits_in_buffer == 16
170
+ word = @bit_buffer & 0xFFFF
171
+ write_byte((word >> 8) & 0xFF)
172
+ write_byte(word & 0xFF)
173
+ end
174
+
175
+ @bit_buffer = 0
176
+ @bits_in_buffer = 0
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Cabriolet
6
+ module Binary
7
+ # CHM ITSF Header (main file header)
8
+ class CHMITSFHeader < BinData::Record
9
+ endian :little
10
+
11
+ string :signature, length: 4 # 'ITSF'
12
+ uint32 :version
13
+ uint32 :header_len
14
+ uint32 :unknown1
15
+ uint32 :timestamp
16
+ uint32 :language_id
17
+ string :guid1, length: 16
18
+ string :guid2, length: 16
19
+ end
20
+
21
+ # CHM Header Section Table
22
+ class CHMHeaderSectionTable < BinData::Record
23
+ endian :little
24
+
25
+ uint64 :offset_hs0
26
+ uint64 :length_hs0
27
+ uint64 :offset_hs1
28
+ uint64 :length_hs1
29
+ uint64 :offset_cs0 # Only in version 3+
30
+ end
31
+
32
+ # CHM Header Section 0
33
+ class CHMHeaderSection0 < BinData::Record
34
+ endian :little
35
+
36
+ uint32 :unknown1
37
+ uint32 :unknown2
38
+ uint64 :file_len
39
+ uint32 :unknown3
40
+ uint32 :unknown4
41
+ end
42
+
43
+ # CHM Header Section 1 (Directory header)
44
+ class CHMHeaderSection1 < BinData::Record
45
+ endian :little
46
+
47
+ string :signature, length: 4 # 'ITSP'
48
+ uint32 :version
49
+ uint32 :header_len
50
+ uint32 :unknown1
51
+ uint32 :chunk_size
52
+ uint32 :density
53
+ uint32 :depth
54
+ int32 :index_root
55
+ uint32 :first_pmgl
56
+ uint32 :last_pmgl
57
+ uint32 :unknown2
58
+ uint32 :num_chunks
59
+ uint32 :language_id
60
+ string :guid, length: 16
61
+ uint32 :unknown3
62
+ uint32 :unknown4
63
+ uint32 :unknown5
64
+ uint32 :unknown6
65
+ end
66
+
67
+ # PMGL Chunk Header (directory listing chunk)
68
+ class PMGLChunkHeader < BinData::Record
69
+ endian :little
70
+
71
+ string :signature, length: 4 # 'PMGL'
72
+ uint32 :quickref_size
73
+ uint32 :unknown1
74
+ int32 :prev_chunk
75
+ int32 :next_chunk
76
+ end
77
+
78
+ # PMGI Chunk Header (directory index chunk)
79
+ class PMGIChunkHeader < BinData::Record
80
+ endian :little
81
+
82
+ string :signature, length: 4 # 'PMGI'
83
+ uint32 :quickref_size
84
+ end
85
+
86
+ # LZX Control Data
87
+ class LZXControlData < BinData::Record
88
+ endian :little
89
+
90
+ uint32 :len
91
+ string :signature, length: 4 # 'LZXC'
92
+ uint32 :version
93
+ uint32 :reset_interval
94
+ uint32 :window_size
95
+ uint32 :cache_size
96
+ uint32 :unknown1
97
+ end
98
+
99
+ # LZX Reset Table Header
100
+ class LZXResetTableHeader < BinData::Record
101
+ endian :little
102
+
103
+ uint32 :unknown1
104
+ uint32 :num_entries
105
+ uint32 :entry_size
106
+ uint32 :table_offset
107
+ uint64 :uncomp_len
108
+ uint64 :comp_len
109
+ uint64 :frame_len
110
+ end
111
+
112
+ # Helper class for reading ENCINT (variable-length integers)
113
+ class ENCINTReader
114
+ # Read an ENCINT from an IO stream
115
+ # Returns the integer value
116
+ def self.read(io)
117
+ result = 0
118
+ byte = 0x80
119
+ bytes_read = 0
120
+ max_bytes = 9 # 63 bits max
121
+
122
+ while byte.anybits?(0x80) && bytes_read < max_bytes
123
+ byte_data = io.read(1)
124
+ if byte_data.nil?
125
+ raise Cabriolet::FormatError,
126
+ "Unexpected end of ENCINT"
127
+ end
128
+
129
+ byte = byte_data.unpack1("C")
130
+ result = (result << 7) | (byte & 0x7F)
131
+ bytes_read += 1
132
+ end
133
+
134
+ if bytes_read == max_bytes && byte.anybits?(0x80)
135
+ raise Cabriolet::FormatError,
136
+ "ENCINT too large"
137
+ end
138
+
139
+ result
140
+ end
141
+
142
+ # Read an ENCINT from a string at a given position
143
+ # Returns [value, new_position]
144
+ def self.read_from_string(str, pos)
145
+ result = 0
146
+ byte = 0x80
147
+ bytes_read = 0
148
+ max_bytes = 9
149
+
150
+ while byte.anybits?(0x80) && bytes_read < max_bytes
151
+ if pos >= str.length
152
+ raise Cabriolet::FormatError,
153
+ "ENCINT beyond string"
154
+ end
155
+
156
+ byte = str.getbyte(pos)
157
+ pos += 1
158
+ result = (result << 7) | (byte & 0x7F)
159
+ bytes_read += 1
160
+ end
161
+
162
+ if bytes_read == max_bytes && byte.anybits?(0x80)
163
+ raise Cabriolet::FormatError,
164
+ "ENCINT too large"
165
+ end
166
+
167
+ [result, pos]
168
+ end
169
+ end
170
+
171
+ # Helper class for writing ENCINT (variable-length integers)
172
+ class ENCINTWriter
173
+ # Write an ENCINT to an IO stream
174
+ # @param io [IO] IO object to write to
175
+ # @param value [Integer] Value to encode
176
+ # @return [Integer] Number of bytes written
177
+ def self.write(io, value)
178
+ bytes = encode(value)
179
+ io.write(bytes)
180
+ bytes.bytesize
181
+ end
182
+
183
+ # Encode an integer as ENCINT bytes
184
+ # @param value [Integer] Value to encode (must be non-negative)
185
+ # @return [String] Encoded bytes
186
+ def self.encode(value)
187
+ if value.negative?
188
+ raise ArgumentError,
189
+ "ENCINT value must be non-negative"
190
+ end
191
+
192
+ # Special case: zero
193
+ return "\x00".b if value.zero?
194
+
195
+ bytes = []
196
+
197
+ # Encode 7 bits at a time
198
+ while value.positive?
199
+ byte = value & 0x7F
200
+ value >>= 7
201
+ bytes.unshift(byte)
202
+ end
203
+
204
+ # Set high bit on all but last byte
205
+ (0...(bytes.length - 1)).each do |i|
206
+ bytes[i] |= 0x80
207
+ end
208
+
209
+ bytes.pack("C*")
210
+ end
211
+ end
212
+ end
213
+ end