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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +243 -368
- data/README.adoc +101 -5
- data/docs/guides/archive-formats/index.adoc +31 -1
- data/docs/guides/archive-formats/ole-format.adoc +316 -0
- data/docs/guides/archive-formats/rpm-format.adoc +249 -0
- data/docs/index.adoc +12 -2
- data/lib/omnizip/algorithms/lzma/distance_coder.rb +29 -18
- data/lib/omnizip/algorithms/lzma/encoder.rb +2 -1
- data/lib/omnizip/algorithms/lzma/length_coder.rb +6 -3
- data/lib/omnizip/algorithms/lzma/literal_decoder.rb +2 -1
- data/lib/omnizip/algorithms/lzma/lzip_decoder.rb +40 -13
- data/lib/omnizip/algorithms/lzma/range_decoder.rb +36 -2
- data/lib/omnizip/algorithms/lzma/range_encoder.rb +19 -0
- data/lib/omnizip/algorithms/lzma/xz_encoder_fast.rb +2 -1
- data/lib/omnizip/algorithms/lzma/xz_utils_decoder.rb +148 -112
- data/lib/omnizip/algorithms/lzma.rb +20 -5
- data/lib/omnizip/algorithms/ppmd7/decoder.rb +25 -21
- data/lib/omnizip/algorithms/ppmd7/encoder.rb +4 -11
- data/lib/omnizip/algorithms/sevenzip_lzma2.rb +2 -1
- data/lib/omnizip/algorithms/xz_lzma2.rb +2 -1
- data/lib/omnizip/algorithms/zstandard/constants.rb +125 -9
- data/lib/omnizip/algorithms/zstandard/decoder.rb +202 -17
- data/lib/omnizip/algorithms/zstandard/encoder.rb +197 -17
- data/lib/omnizip/algorithms/zstandard/frame/block.rb +128 -0
- data/lib/omnizip/algorithms/zstandard/frame/header.rb +224 -0
- data/lib/omnizip/algorithms/zstandard/fse/bitstream.rb +186 -0
- data/lib/omnizip/algorithms/zstandard/fse/encoder.rb +325 -0
- data/lib/omnizip/algorithms/zstandard/fse/table.rb +269 -0
- data/lib/omnizip/algorithms/zstandard/huffman.rb +272 -0
- data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +339 -0
- data/lib/omnizip/algorithms/zstandard/literals.rb +178 -0
- data/lib/omnizip/algorithms/zstandard/literals_encoder.rb +251 -0
- data/lib/omnizip/algorithms/zstandard/sequences.rb +346 -0
- data/lib/omnizip/buffer/memory_extractor.rb +3 -3
- data/lib/omnizip/buffer.rb +2 -2
- data/lib/omnizip/filters/delta.rb +2 -1
- data/lib/omnizip/filters/registry.rb +6 -6
- data/lib/omnizip/formats/cpio/bounded_io.rb +66 -0
- data/lib/omnizip/formats/lzip.rb +2 -1
- data/lib/omnizip/formats/lzma_alone.rb +2 -1
- data/lib/omnizip/formats/ole/allocation_table.rb +244 -0
- data/lib/omnizip/formats/ole/constants.rb +61 -0
- data/lib/omnizip/formats/ole/dirent.rb +380 -0
- data/lib/omnizip/formats/ole/header.rb +198 -0
- data/lib/omnizip/formats/ole/ranges_io.rb +264 -0
- data/lib/omnizip/formats/ole/storage.rb +305 -0
- data/lib/omnizip/formats/ole/types/variant.rb +328 -0
- data/lib/omnizip/formats/ole.rb +145 -0
- data/lib/omnizip/formats/rar/compression/ppmd/decoder.rb +92 -49
- data/lib/omnizip/formats/rar/compression/ppmd/encoder.rb +13 -20
- data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +6 -2
- data/lib/omnizip/formats/rar3/reader.rb +6 -2
- data/lib/omnizip/formats/rar5/reader.rb +4 -1
- data/lib/omnizip/formats/rpm/constants.rb +58 -0
- data/lib/omnizip/formats/rpm/entry.rb +102 -0
- data/lib/omnizip/formats/rpm/header.rb +113 -0
- data/lib/omnizip/formats/rpm/lead.rb +122 -0
- data/lib/omnizip/formats/rpm/tag.rb +230 -0
- data/lib/omnizip/formats/rpm.rb +434 -0
- data/lib/omnizip/formats/seven_zip/bcj2_stream_decompressor.rb +239 -0
- data/lib/omnizip/formats/seven_zip/coder_chain.rb +32 -8
- data/lib/omnizip/formats/seven_zip/constants.rb +1 -1
- data/lib/omnizip/formats/seven_zip/reader.rb +84 -8
- data/lib/omnizip/formats/seven_zip/stream_compressor.rb +2 -1
- data/lib/omnizip/formats/seven_zip/stream_decompressor.rb +6 -0
- data/lib/omnizip/formats/seven_zip/writer.rb +21 -9
- data/lib/omnizip/formats/seven_zip.rb +10 -0
- data/lib/omnizip/formats/xar/entry.rb +18 -5
- data/lib/omnizip/formats/xar/header.rb +34 -6
- data/lib/omnizip/formats/xar/reader.rb +43 -10
- data/lib/omnizip/formats/xar/toc.rb +34 -21
- data/lib/omnizip/formats/xar/writer.rb +15 -5
- data/lib/omnizip/formats/xz_impl/block_decoder.rb +45 -33
- data/lib/omnizip/formats/xz_impl/block_encoder.rb +2 -1
- data/lib/omnizip/formats/xz_impl/index_decoder.rb +3 -1
- data/lib/omnizip/formats/xz_impl/stream_header_parser.rb +2 -1
- data/lib/omnizip/formats/zip/end_of_central_directory.rb +4 -3
- data/lib/omnizip/implementations/seven_zip/lzma/decoder.rb +14 -6
- data/lib/omnizip/implementations/seven_zip/lzma/encoder.rb +2 -1
- data/lib/omnizip/implementations/seven_zip/lzma2/encoder.rb +28 -13
- data/lib/omnizip/implementations/xz_utils/lzma2/encoder.rb +13 -6
- data/lib/omnizip/pipe/stream_compressor.rb +1 -1
- data/lib/omnizip/version.rb +1 -1
- data/readme-docs/compression-algorithms.adoc +6 -2
- 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
|