omnizip 0.3.2 → 0.3.4

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +243 -368
  3. data/README.adoc +101 -5
  4. data/docs/guides/archive-formats/index.adoc +31 -1
  5. data/docs/guides/archive-formats/ole-format.adoc +316 -0
  6. data/docs/guides/archive-formats/rpm-format.adoc +249 -0
  7. data/docs/index.adoc +12 -2
  8. data/lib/omnizip/algorithms/lzma/distance_coder.rb +29 -18
  9. data/lib/omnizip/algorithms/lzma/encoder.rb +2 -1
  10. data/lib/omnizip/algorithms/lzma/length_coder.rb +6 -3
  11. data/lib/omnizip/algorithms/lzma/literal_decoder.rb +2 -1
  12. data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +40 -13
  13. data/lib/omnizip/algorithms/lzma/range_decoder.rb +36 -2
  14. data/lib/omnizip/algorithms/lzma/range_encoder.rb +19 -0
  15. data/lib/omnizip/algorithms/lzma/xz_encoder_fast.rb +2 -1
  16. data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +148 -112
  17. data/lib/omnizip/algorithms/lzma.rb +20 -5
  18. data/lib/omnizip/algorithms/ppmd7/decoder.rb +25 -21
  19. data/lib/omnizip/algorithms/ppmd7/encoder.rb +4 -11
  20. data/lib/omnizip/algorithms/sevenzip_lzma2.rb +2 -1
  21. data/lib/omnizip/algorithms/xz_lzma2.rb +2 -1
  22. data/lib/omnizip/algorithms/zstandard/constants.rb +125 -9
  23. data/lib/omnizip/algorithms/zstandard/decoder.rb +202 -17
  24. data/lib/omnizip/algorithms/zstandard/encoder.rb +197 -17
  25. data/lib/omnizip/algorithms/zstandard/frame/block.rb +128 -0
  26. data/lib/omnizip/algorithms/zstandard/frame/header.rb +224 -0
  27. data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +186 -0
  28. data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +325 -0
  29. data/lib/omnizip/algorithms/zstandard/fse/table.rb +269 -0
  30. data/lib/omnizip/algorithms/zstandard/huffman.rb +272 -0
  31. data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +339 -0
  32. data/lib/omnizip/algorithms/zstandard/literals.rb +178 -0
  33. data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +251 -0
  34. data/lib/omnizip/algorithms/zstandard/sequences.rb +346 -0
  35. data/lib/omnizip/buffer/memory_extractor.rb +3 -3
  36. data/lib/omnizip/buffer.rb +2 -2
  37. data/lib/omnizip/filters/delta.rb +2 -1
  38. data/lib/omnizip/filters/registry.rb +6 -6
  39. data/lib/omnizip/formats/cpio/bounded_io.rb +66 -0
  40. data/lib/omnizip/formats/lzip.rb +2 -1
  41. data/lib/omnizip/formats/lzma_alone.rb +2 -1
  42. data/lib/omnizip/formats/ole/allocation_table.rb +244 -0
  43. data/lib/omnizip/formats/ole/constants.rb +61 -0
  44. data/lib/omnizip/formats/ole/dirent.rb +380 -0
  45. data/lib/omnizip/formats/ole/header.rb +198 -0
  46. data/lib/omnizip/formats/ole/ranges_io.rb +264 -0
  47. data/lib/omnizip/formats/ole/storage.rb +305 -0
  48. data/lib/omnizip/formats/ole/types/variant.rb +328 -0
  49. data/lib/omnizip/formats/ole.rb +145 -0
  50. data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +92 -49
  51. data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +13 -20
  52. data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +6 -2
  53. data/lib/omnizip/formats/rar3/reader.rb +6 -2
  54. data/lib/omnizip/formats/rar5/reader.rb +4 -1
  55. data/lib/omnizip/formats/rpm/constants.rb +58 -0
  56. data/lib/omnizip/formats/rpm/entry.rb +102 -0
  57. data/lib/omnizip/formats/rpm/header.rb +113 -0
  58. data/lib/omnizip/formats/rpm/lead.rb +122 -0
  59. data/lib/omnizip/formats/rpm/tag.rb +230 -0
  60. data/lib/omnizip/formats/rpm.rb +434 -0
  61. data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +239 -0
  62. data/lib/omnizip/formats/seven_zip/coder_chain.rb +32 -8
  63. data/lib/omnizip/formats/seven_zip/constants.rb +1 -1
  64. data/lib/omnizip/formats/seven_zip/reader.rb +84 -8
  65. data/lib/omnizip/formats/seven_zip/stream_compressor.rb +2 -1
  66. data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +6 -0
  67. data/lib/omnizip/formats/seven_zip/writer.rb +21 -9
  68. data/lib/omnizip/formats/seven_zip.rb +10 -0
  69. data/lib/omnizip/formats/xar/entry.rb +18 -5
  70. data/lib/omnizip/formats/xar/header.rb +34 -6
  71. data/lib/omnizip/formats/xar/reader.rb +43 -10
  72. data/lib/omnizip/formats/xar/toc.rb +34 -21
  73. data/lib/omnizip/formats/xar/writer.rb +15 -5
  74. data/lib/omnizip/formats/xz_impl/block_decoder.rb +45 -33
  75. data/lib/omnizip/formats/xz_impl/block_encoder.rb +2 -1
  76. data/lib/omnizip/formats/xz_impl/index_decoder.rb +3 -1
  77. data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +2 -1
  78. data/lib/omnizip/formats/zip/end_of_central_directory.rb +4 -3
  79. data/lib/omnizip/implementations/seven_zip/lzma/decoder.rb +14 -6
  80. data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +2 -1
  81. data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +28 -13
  82. data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +13 -6
  83. data/lib/omnizip/pipe/stream_compressor.rb +1 -1
  84. data/lib/omnizip/version.rb +1 -1
  85. data/readme-docs/compression-algorithms.adoc +6 -2
  86. metadata +30 -2
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+ require_relative "header"
5
+ require_relative "allocation_table"
6
+ require_relative "dirent"
7
+ require_relative "ranges_io"
8
+
9
+ module Omnizip
10
+ module Formats
11
+ module Ole
12
+ # OLE compound document storage
13
+ #
14
+ # Main class for reading and writing OLE compound documents.
15
+ # Provides access to the hierarchical file structure within the document.
16
+ class Storage
17
+ include Constants
18
+
19
+ # OLE format error
20
+ class FormatError < StandardError; end
21
+
22
+ # @return [IO] Underlying IO object
23
+ attr_reader :io
24
+
25
+ # @return [Boolean] Whether to close IO on #close
26
+ attr_reader :close_parent
27
+
28
+ # @return [Boolean] Whether opened for writing
29
+ attr_reader :writeable
30
+
31
+ # @return [Header] Parsed header
32
+ attr_reader :header
33
+
34
+ # @return [AllocationTable::Big] Big block allocation table
35
+ attr_reader :bbat
36
+
37
+ # @return [AllocationTable::Small] Small block allocation table
38
+ attr_reader :sbat
39
+
40
+ # @return [RangesIO] Small block file
41
+ attr_reader :sb_file
42
+
43
+ # @return [Dirent] Root entry
44
+ attr_reader :root
45
+
46
+ # @return [Array<Dirent>] All dirents (flat list)
47
+ attr_reader :dirents
48
+
49
+ # Open OLE file
50
+ #
51
+ # @param path_or_io [String, IO] File path or IO object
52
+ # @param mode [String, nil] Open mode
53
+ # @yield [Storage]
54
+ # @return [Storage]
55
+ def self.open(path_or_io, mode = nil)
56
+ storage = new(path_or_io, mode)
57
+ if block_given?
58
+ begin
59
+ yield storage
60
+ ensure
61
+ storage.close
62
+ end
63
+ else
64
+ storage
65
+ end
66
+ end
67
+
68
+ # Initialize storage
69
+ #
70
+ # @param path_or_io [String, IO] File path or IO object
71
+ # @param mode [String, nil] Open mode
72
+ def initialize(path_or_io, mode = nil)
73
+ @close_parent, @io = if path_or_io.is_a?(String)
74
+ mode ||= "rb"
75
+ [true, File.open(path_or_io, mode)]
76
+ else
77
+ raise ArgumentError, "Cannot specify mode with IO object" if mode
78
+
79
+ [false, path_or_io]
80
+ end
81
+
82
+ # Force binary encoding
83
+ @io.set_encoding(Encoding::ASCII_8BIT) if @io.respond_to?(:set_encoding)
84
+
85
+ # Determine if writable
86
+ @writeable = determine_writeable(mode)
87
+
88
+ @sb_file = nil
89
+
90
+ # Load or create
91
+ if @io.size.positive?
92
+ load
93
+ else
94
+ create_empty
95
+ end
96
+ end
97
+
98
+ # Load OLE document from IO
99
+ def load
100
+ @io.rewind
101
+ header_block = @io.read(HEADER_BLOCK_SIZE)
102
+
103
+ # Parse header
104
+ @header = Header.parse(header_block)
105
+
106
+ # Build BBAT chain from header
107
+ @bbat = AllocationTable::Big.new(self)
108
+ bbat_chain = header_block[HEADER_SIZE..].unpack("V*")
109
+
110
+ # Add Meta BAT blocks if present
111
+ mbat_block = @header.mbat_start
112
+ @header.num_mbat.times do
113
+ blocks = @bbat.read([mbat_block]).unpack("V*")
114
+ mbat_block = blocks.pop
115
+ bbat_chain += blocks
116
+ end
117
+
118
+ # Load BBAT
119
+ @bbat.load(@bbat.read(bbat_chain[0, @header.num_bat]))
120
+
121
+ # Load dirents
122
+ raw_dirents = @bbat.read(@header.dirent_start)
123
+ @dirents = []
124
+ (raw_dirents.bytesize / DIRENT_SIZE).times do |i|
125
+ dirent_data = raw_dirents.byteslice(i * DIRENT_SIZE, DIRENT_SIZE)
126
+ @dirents << Dirent.parse(self, dirent_data)
127
+ end
128
+
129
+ # Build tree structure
130
+ @root = build_tree(@dirents).first
131
+
132
+ # Remove empty entries
133
+ @dirents.reject!(&:empty?)
134
+
135
+ # Setup SBAT
136
+ @sb_file = RangesIOResizeable.new(@bbat, first_block: @root.first_block, size: @root.size)
137
+ @sbat = AllocationTable::Small.new(self)
138
+ @sbat.load(@bbat.read(@header.sbat_start))
139
+ end
140
+
141
+ # Build tree from flat dirent list
142
+ #
143
+ # @param dirents [Array<Dirent>]
144
+ # @param idx [Integer]
145
+ # @return [Array<Dirent>]
146
+ def build_tree(dirents, idx = 0)
147
+ return [] if idx == EOT
148
+
149
+ dirent = dirents[idx]
150
+
151
+ # Build children recursively
152
+ build_tree(dirents, dirent.child).each { |child| dirent << child }
153
+
154
+ # Set index
155
+ dirent.idx = idx
156
+
157
+ # Return list for tree building
158
+ build_tree(dirents, dirent.prev) + [dirent] + build_tree(dirents, dirent.next)
159
+ end
160
+
161
+ # Close storage
162
+ def close
163
+ @sb_file&.close
164
+ @io.close if @close_parent
165
+ end
166
+
167
+ # Get appropriate BAT for size
168
+ #
169
+ # @param size [Integer] File size
170
+ # @return [AllocationTable]
171
+ def bat_for_size(size)
172
+ size >= @header.threshold ? @bbat : @sbat
173
+ end
174
+
175
+ # List entries at path
176
+ #
177
+ # @param path [String] Directory path
178
+ # @return [Array<String>]
179
+ def list(path = "/")
180
+ dirent = find_dirent(path)
181
+ return [] unless dirent
182
+
183
+ dirent.children.map(&:name)
184
+ end
185
+
186
+ # Read file content
187
+ #
188
+ # @param path [String] File path
189
+ # @return [String]
190
+ def read(path)
191
+ dirent = find_dirent(path)
192
+ raise Errno::ENOENT, path unless dirent
193
+ raise Errno::EISDIR, path if dirent.dir?
194
+
195
+ dirent.read
196
+ end
197
+
198
+ # Check if entry exists
199
+ #
200
+ # @param path [String]
201
+ # @return [Boolean]
202
+ def exist?(path)
203
+ !find_dirent(path).nil?
204
+ end
205
+
206
+ alias exists? :exist?
207
+
208
+ # Check if path is a file
209
+ #
210
+ # @param path [String]
211
+ # @return [Boolean]
212
+ def file?(path)
213
+ dirent = find_dirent(path)
214
+ dirent&.file?
215
+ end
216
+
217
+ # Check if path is a directory
218
+ #
219
+ # @param path [String]
220
+ # @return [Boolean]
221
+ def directory?(path)
222
+ dirent = find_dirent(path)
223
+ dirent&.dir?
224
+ end
225
+
226
+ # Get entry info
227
+ #
228
+ # @param path [String]
229
+ # @return [Hash, nil]
230
+ def info(path)
231
+ dirent = find_dirent(path)
232
+ return nil unless dirent
233
+
234
+ {
235
+ name: dirent.name,
236
+ type: dirent.type,
237
+ size: dirent.size,
238
+ create_time: dirent.create_time,
239
+ modify_time: dirent.modify_time,
240
+ }
241
+ end
242
+
243
+ # Find dirent by path
244
+ #
245
+ # @param path [String]
246
+ # @return [Dirent, nil]
247
+ def find_dirent(path)
248
+ path = path.to_s
249
+ path = path[1..] if path.start_with?("/")
250
+
251
+ return @root if path.empty?
252
+
253
+ parts = path.split("/")
254
+ current = @root
255
+
256
+ parts.each do |part|
257
+ next if part.empty?
258
+ return nil if current.file?
259
+
260
+ current = current / part
261
+ return nil unless current
262
+ end
263
+
264
+ current
265
+ end
266
+
267
+ # Inspect
268
+ def inspect
269
+ "#<#{self.class} io=#{@io.inspect} root=#{@root.inspect}>"
270
+ end
271
+
272
+ private
273
+
274
+ # Determine if IO is writable
275
+ def determine_writeable(mode)
276
+ return false if mode&.include?("r") && !mode.include?("+")
277
+
278
+ if mode
279
+ mode.include?("w") || mode.include?("a") || mode.include?("+")
280
+ else
281
+ begin
282
+ @io.flush
283
+ @io.write_nonblock("") if @io.respond_to?(:write_nonblock)
284
+ true
285
+ rescue IOError, Errno::EBADF
286
+ false
287
+ end
288
+ end
289
+ end
290
+
291
+ # Create empty OLE document
292
+ def create_empty
293
+ @header = Header.create
294
+ @bbat = AllocationTable::Big.new(self)
295
+ @root = Dirent.create(self, type: :root, name: "Root Entry")
296
+ @root.idx = 0
297
+ @dirents = [@root]
298
+ @sb_file = RangesIOResizeable.new(@bbat, first_block: EOC)
299
+ @sbat = AllocationTable::Small.new(self)
300
+ @io.truncate(0)
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Omnizip
6
+ module Formats
7
+ module Ole
8
+ # OLE type serialization module
9
+ #
10
+ # Provides serialization and deserialization for OLE data types
11
+ # including variant types, strings, timestamps, and GUIDs.
12
+ module Types
13
+ # Generic binary data handler
14
+ class Data < String
15
+ # Load from binary data
16
+ def self.load(str)
17
+ new(str)
18
+ end
19
+
20
+ # Dump to binary data
21
+ def self.dump(str)
22
+ str.to_s
23
+ end
24
+ end
25
+
26
+ # Null-terminated ASCII string (VT_LPSTR)
27
+ class Lpstr < String
28
+ # Load from binary data
29
+ def self.load(str)
30
+ new(str.to_s.chomp("\x00"))
31
+ end
32
+
33
+ # Dump to binary data
34
+ def self.dump(str)
35
+ str.to_s
36
+ end
37
+ end
38
+
39
+ # Null-terminated UTF-16LE string (VT_LPWSTR)
40
+ class Lpwstr < String
41
+ # Load from UTF-16LE binary data
42
+ def self.load(str)
43
+ return new("") if str.nil? || str.empty?
44
+
45
+ # Decode UTF-16LE to UTF-8, strip null terminator
46
+ decoded = str.encode(Encoding::UTF_8, Encoding::UTF_16LE)
47
+ new(decoded.chomp("\x00"))
48
+ end
49
+
50
+ # Dump to UTF-16LE binary data
51
+ def self.dump(str)
52
+ return "\x00\x00".b if str.nil? || str.empty?
53
+
54
+ # Encode UTF-8 to UTF-16LE and force to binary
55
+ data = str.encode(Encoding::UTF_16LE).force_encoding(Encoding::ASCII_8BIT)
56
+ # Add null terminator (single UTF-16 null character = 2 bytes)
57
+ data + "\x00\x00".b
58
+ end
59
+ end
60
+
61
+ # Windows FILETIME timestamp (VT_FILETIME)
62
+ #
63
+ # Represents time as 100-nanosecond intervals since January 1, 1601.
64
+ class FileTime
65
+ # Size in bytes
66
+ SIZE = 8
67
+
68
+ # Windows epoch (January 1, 1601)
69
+ EPOCH = DateTime.new(1601, 1, 1)
70
+
71
+ # @return [DateTime] The timestamp value
72
+ attr_reader :value
73
+
74
+ # Create FileTime from DateTime
75
+ #
76
+ # @param value [DateTime, Time, nil]
77
+ def initialize(value = nil)
78
+ @value = case value
79
+ when DateTime, nil
80
+ value
81
+ when Time
82
+ DateTime.new(value.year, value.month, value.day,
83
+ value.hour, value.min, value.sec)
84
+ else
85
+ raise ArgumentError, "Invalid time value: #{value.class}"
86
+ end
87
+ end
88
+
89
+ # Load from binary data
90
+ #
91
+ # @param str [String] 8-byte FILETIME data
92
+ # @return [FileTime, nil] Parsed timestamp or nil if zero
93
+ def self.load(str)
94
+ return nil if str.nil? || str.bytesize < SIZE
95
+
96
+ low, high = str.unpack("V2")
97
+ return nil if low.zero? && high.zero?
98
+
99
+ # Convert 100-nanosecond intervals to seconds
100
+ intervals = (high << 32) | low
101
+ seconds = intervals / 10_000_000.0
102
+
103
+ # Add to epoch
104
+ begin
105
+ value = EPOCH + (seconds / 86_400.0)
106
+ new(value)
107
+ rescue StandardError
108
+ nil
109
+ end
110
+ end
111
+
112
+ # Dump to binary data
113
+ #
114
+ # @param time [FileTime, DateTime, Time, nil]
115
+ # @return [String] 8-byte FILETIME data
116
+ def self.dump(time)
117
+ return "\x00".b * SIZE if time.nil?
118
+
119
+ case time
120
+ when FileTime
121
+ value = time.value
122
+ when DateTime
123
+ value = time
124
+ when Time
125
+ value = DateTime.new(time.year, time.month, time.day,
126
+ time.hour, time.min, time.sec)
127
+ else
128
+ raise ArgumentError, "Invalid time argument: #{time.class}"
129
+ end
130
+
131
+ # Calculate nanoseconds since epoch
132
+ days = (value - EPOCH).to_f
133
+ nanoseconds = (days * 86_400_000_000_000).round
134
+
135
+ high = nanoseconds >> 32
136
+ low = nanoseconds & 0xFFFFFFFF
137
+
138
+ [low, high].pack("V2")
139
+ end
140
+
141
+ # Convert to Time
142
+ #
143
+ # @return [Time]
144
+ def to_time
145
+ return nil if @value.nil?
146
+
147
+ Time.new(@value.year, @value.month, @value.day,
148
+ @value.hour, @value.min, @value.sec)
149
+ end
150
+
151
+ # Convert to string
152
+ #
153
+ # @return [String]
154
+ def to_s
155
+ @value.to_s
156
+ end
157
+
158
+ # Inspect
159
+ def inspect
160
+ "#<#{self.class}: #{@value}>"
161
+ end
162
+ end
163
+
164
+ # COM CLSID/GUID (VT_CLSID)
165
+ #
166
+ # 128-bit globally unique identifier.
167
+ class Clsid < String
168
+ # Size in bytes
169
+ SIZE = 16
170
+
171
+ # Pack format for GUID components
172
+ PACK = "V v v CC C6"
173
+
174
+ # Load from binary data
175
+ #
176
+ # @param str [String] 16-byte GUID data
177
+ # @return [Clsid]
178
+ def self.load(str)
179
+ new(str.to_s)
180
+ end
181
+
182
+ # Dump to binary data
183
+ #
184
+ # @param guid [String, nil] GUID string or binary data
185
+ # @return [String] 16-byte binary data
186
+ def self.dump(guid)
187
+ return "\x00".b * SIZE if guid.nil?
188
+
189
+ # If it contains dashes, parse from string format
190
+ guid.include?("-") ? parse(guid) : guid.to_s
191
+ end
192
+
193
+ # Parse from string format "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
194
+ #
195
+ # @param str [String] GUID string
196
+ # @return [Clsid]
197
+ # @raise [ArgumentError] If format is invalid
198
+ def self.parse(str)
199
+ values = str.scan(/[a-f\d]+/i).map(&:hex)
200
+
201
+ if values.length == 5
202
+ # Split 4th and 5th groups into bytes
203
+ values[3] = sprintf("%04x", values[3]).scan(/../).map(&:hex)
204
+ values[4] = sprintf("%012x", values[4]).scan(/../).map(&:hex)
205
+ guid = new(values.flatten.pack(PACK))
206
+
207
+ if guid.format.delete("{}").downcase == str.downcase.delete("{}")
208
+ return guid
209
+ end
210
+ end
211
+
212
+ raise ArgumentError, "Invalid GUID format: #{str}"
213
+ end
214
+
215
+ # Format to human-readable string
216
+ #
217
+ # @return [String] "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
218
+ def format
219
+ vals = unpack(PACK)
220
+ # vals = [uint32, uint16, uint16, uint8, uint8, uint8, uint8, uint8, uint8, uint8, uint8]
221
+ last_six = vals[5, 6].map { |b| sprintf("%02x", b) }.join
222
+ sprintf("%08x-%04x-%04x-%02x%02x-%s", vals[0], vals[1], vals[2], vals[3], vals[4], last_six)
223
+ end
224
+
225
+ # Inspect
226
+ def inspect
227
+ "#<#{self.class}:{#{format}}>"
228
+ end
229
+ end
230
+
231
+ # Variant type constants
232
+ module Variant
233
+ # Type ID to name mapping
234
+ NAMES = {
235
+ 0x0000 => "VT_EMPTY",
236
+ 0x0001 => "VT_NULL",
237
+ 0x0002 => "VT_I2",
238
+ 0x0003 => "VT_I4",
239
+ 0x0004 => "VT_R4",
240
+ 0x0005 => "VT_R8",
241
+ 0x0006 => "VT_CY",
242
+ 0x0007 => "VT_DATE",
243
+ 0x0008 => "VT_BSTR",
244
+ 0x0009 => "VT_DISPATCH",
245
+ 0x000a => "VT_ERROR",
246
+ 0x000b => "VT_BOOL",
247
+ 0x000c => "VT_VARIANT",
248
+ 0x000d => "VT_UNKNOWN",
249
+ 0x000e => "VT_DECIMAL",
250
+ 0x0010 => "VT_I1",
251
+ 0x0011 => "VT_UI1",
252
+ 0x0012 => "VT_UI2",
253
+ 0x0013 => "VT_UI4",
254
+ 0x0014 => "VT_I8",
255
+ 0x0015 => "VT_UI8",
256
+ 0x0016 => "VT_INT",
257
+ 0x0017 => "VT_UINT",
258
+ 0x0018 => "VT_VOID",
259
+ 0x0019 => "VT_HRESULT",
260
+ 0x001a => "VT_PTR",
261
+ 0x001b => "VT_SAFEARRAY",
262
+ 0x001c => "VT_CARRAY",
263
+ 0x001d => "VT_USERDEFINED",
264
+ 0x001e => "VT_LPSTR",
265
+ 0x001f => "VT_LPWSTR",
266
+ 0x0040 => "VT_FILETIME",
267
+ 0x0041 => "VT_BLOB",
268
+ 0x0042 => "VT_STREAM",
269
+ 0x0043 => "VT_STORAGE",
270
+ 0x0044 => "VT_STREAMED_OBJECT",
271
+ 0x0045 => "VT_STORED_OBJECT",
272
+ 0x0046 => "VT_BLOB_OBJECT",
273
+ 0x0047 => "VT_CF",
274
+ 0x0048 => "VT_CLSID",
275
+ 0x0fff => "VT_ILLEGALMASKED",
276
+ 0x1000 => "VT_VECTOR",
277
+ 0x2000 => "VT_ARRAY",
278
+ 0x4000 => "VT_BYREF",
279
+ 0x8000 => "VT_RESERVED",
280
+ 0xffff => "VT_ILLEGAL",
281
+ }.freeze
282
+
283
+ # Type name to class mapping
284
+ CLASS_MAP = {
285
+ "VT_LPSTR" => Lpstr,
286
+ "VT_LPWSTR" => Lpwstr,
287
+ "VT_FILETIME" => FileTime,
288
+ "VT_CLSID" => Clsid,
289
+ }.freeze
290
+
291
+ # Define type constants
292
+ NAMES.each do |num, name|
293
+ const_set name, num
294
+ end
295
+
296
+ # Additional constant
297
+ VT_TYPEMASK = 0x0fff
298
+
299
+ # Load variant value from binary data
300
+ #
301
+ # @param type [Integer] Variant type ID
302
+ # @param str [String] Binary data
303
+ # @return [Object] Deserialized value
304
+ def self.load(type, str)
305
+ type_name = NAMES[type]
306
+ raise ArgumentError, "Unknown OLE type: 0x#{format('%04x', type)}" unless type_name
307
+
308
+ klass = CLASS_MAP[type_name] || Data
309
+ klass.load(str)
310
+ end
311
+
312
+ # Dump variant value to binary data
313
+ #
314
+ # @param type [Integer] Variant type ID
315
+ # @param value [Object] Value to serialize
316
+ # @return [String] Binary data
317
+ def self.dump(type, value)
318
+ type_name = NAMES[type]
319
+ raise ArgumentError, "Unknown OLE type: 0x#{format('%04x', type)}" unless type_name
320
+
321
+ klass = CLASS_MAP[type_name] || Data
322
+ klass.dump(value)
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end