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,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "constants"
|
|
5
|
+
|
|
6
|
+
module Omnizip
|
|
7
|
+
module Formats
|
|
8
|
+
module Ole
|
|
9
|
+
# OLE allocation table
|
|
10
|
+
#
|
|
11
|
+
# Manages block chains for files stored in OLE containers.
|
|
12
|
+
# There are two types: Big (BBAT) and Small (SBAT).
|
|
13
|
+
class AllocationTable
|
|
14
|
+
include Constants
|
|
15
|
+
|
|
16
|
+
# @return [Array<Integer>] Table entries
|
|
17
|
+
attr_reader :entries
|
|
18
|
+
|
|
19
|
+
# @return [Object] Parent OLE storage
|
|
20
|
+
attr_reader :ole
|
|
21
|
+
|
|
22
|
+
# @return [IO] Underlying IO
|
|
23
|
+
attr_reader :io
|
|
24
|
+
|
|
25
|
+
# @return [Integer] Block size in bytes
|
|
26
|
+
attr_reader :block_size
|
|
27
|
+
|
|
28
|
+
# Initialize allocation table
|
|
29
|
+
#
|
|
30
|
+
# @param ole [Object] Parent OLE storage
|
|
31
|
+
def initialize(ole)
|
|
32
|
+
@ole = ole
|
|
33
|
+
@entries = []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Load allocation table from binary data
|
|
37
|
+
#
|
|
38
|
+
# @param data [String] Binary data containing table entries
|
|
39
|
+
def load(data)
|
|
40
|
+
@entries = data.unpack("V*")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get entry at index
|
|
44
|
+
#
|
|
45
|
+
# @param idx [Integer] Entry index
|
|
46
|
+
# @return [Integer] Entry value
|
|
47
|
+
def [](idx)
|
|
48
|
+
@entries[idx]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Set entry at index
|
|
52
|
+
#
|
|
53
|
+
# @param idx [Integer] Entry index
|
|
54
|
+
# @param val [Integer] Entry value
|
|
55
|
+
def []=(idx, val)
|
|
56
|
+
@entries[idx] = val
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get number of entries
|
|
60
|
+
#
|
|
61
|
+
# @return [Integer] Entry count
|
|
62
|
+
def length
|
|
63
|
+
@entries.length
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Follow chain from starting index
|
|
67
|
+
#
|
|
68
|
+
# @param idx [Integer] Starting block index
|
|
69
|
+
# @return [Array<Integer>] Chain of block indices
|
|
70
|
+
# @raise [ArgumentError] If chain is broken
|
|
71
|
+
def chain(idx)
|
|
72
|
+
result = []
|
|
73
|
+
visited = Set.new
|
|
74
|
+
|
|
75
|
+
until idx >= META_BAT
|
|
76
|
+
if idx.negative? || idx > length
|
|
77
|
+
raise ArgumentError, "Broken allocation chain at index #{idx}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if visited.include?(idx)
|
|
81
|
+
raise ArgumentError, "Circular chain detected at index #{idx}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
visited << idx
|
|
85
|
+
result << idx
|
|
86
|
+
idx = @entries[idx]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Convert block chain to byte ranges
|
|
93
|
+
#
|
|
94
|
+
# @param blocks [Array<Integer>] Block indices
|
|
95
|
+
# @param size [Integer, nil] Optional size to truncate to
|
|
96
|
+
# @return [Array<Array<Integer, Integer>>] Array of [offset, length] pairs
|
|
97
|
+
def blocks_to_ranges(blocks, size = nil)
|
|
98
|
+
return [] if blocks.empty?
|
|
99
|
+
|
|
100
|
+
# Truncate chain if size specified
|
|
101
|
+
blocks = blocks[0, (size.to_f / block_size).ceil] if size
|
|
102
|
+
|
|
103
|
+
# Convert to ranges
|
|
104
|
+
ranges = blocks.map { |i| [block_size * i, block_size] }
|
|
105
|
+
|
|
106
|
+
# Truncate final range if needed
|
|
107
|
+
if ranges.last && size
|
|
108
|
+
ranges.last[1] -= ((ranges.length * block_size) - size)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
ranges
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get ranges for a chain
|
|
115
|
+
#
|
|
116
|
+
# @param chain_or_idx [Array<Integer>, Integer] Block chain or head index
|
|
117
|
+
# @param size [Integer, nil] Optional size
|
|
118
|
+
# @return [Array<Array<Integer, Integer>>] Byte ranges
|
|
119
|
+
def ranges(chain_or_idx, size = nil)
|
|
120
|
+
blocks = chain_or_idx.is_a?(Array) ? chain_or_idx : chain(chain_or_idx)
|
|
121
|
+
blocks_to_ranges(blocks, size)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Read data from block chain
|
|
125
|
+
#
|
|
126
|
+
# @param chain_or_idx [Array<Integer>, Integer] Block chain or head index
|
|
127
|
+
# @param size [Integer, nil] Optional size
|
|
128
|
+
# @return [String] Data from chain
|
|
129
|
+
def read(chain_or_idx, size = nil)
|
|
130
|
+
ranges = self.ranges(chain_or_idx, size)
|
|
131
|
+
data = "".b
|
|
132
|
+
|
|
133
|
+
ranges.each do |offset, len|
|
|
134
|
+
@io.seek(offset)
|
|
135
|
+
data << @io.read(len).to_s
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
data
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Find a free block
|
|
142
|
+
#
|
|
143
|
+
# @return [Integer] Free block index
|
|
144
|
+
def free_block
|
|
145
|
+
idx = @entries.index(AVAIL)
|
|
146
|
+
return idx if idx
|
|
147
|
+
|
|
148
|
+
@entries << AVAIL
|
|
149
|
+
@entries.length - 1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Resize a block chain
|
|
153
|
+
#
|
|
154
|
+
# @param blocks [Array<Integer>] Current blocks (modified in place)
|
|
155
|
+
# @param size [Integer] New size in bytes
|
|
156
|
+
# @return [Array<Integer>] Updated blocks
|
|
157
|
+
def resize_chain(blocks, size)
|
|
158
|
+
new_num_blocks = (size / block_size.to_f).ceil
|
|
159
|
+
old_num_blocks = blocks.length
|
|
160
|
+
|
|
161
|
+
if new_num_blocks < old_num_blocks
|
|
162
|
+
# De-allocate excess blocks
|
|
163
|
+
(new_num_blocks...old_num_blocks).each { |i| @entries[blocks[i]] = AVAIL }
|
|
164
|
+
@entries[blocks[new_num_blocks - 1]] = EOC if new_num_blocks.positive?
|
|
165
|
+
blocks.slice!(new_num_blocks..-1)
|
|
166
|
+
elsif new_num_blocks > old_num_blocks
|
|
167
|
+
# Allocate more blocks
|
|
168
|
+
last_block = blocks.last
|
|
169
|
+
(new_num_blocks - old_num_blocks).times do
|
|
170
|
+
block = free_block
|
|
171
|
+
@entries[last_block] = block if last_block
|
|
172
|
+
blocks << block
|
|
173
|
+
last_block = block
|
|
174
|
+
@entries[last_block] = EOC
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
blocks
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Truncate table to remove trailing AVAIL entries
|
|
182
|
+
#
|
|
183
|
+
# @return [Array<Integer>] Truncated entries
|
|
184
|
+
def truncate
|
|
185
|
+
temp = @entries.reverse
|
|
186
|
+
first_non_avail = temp.find { |b| b != AVAIL }
|
|
187
|
+
temp = temp[temp.index(first_non_avail)..] if first_non_avail
|
|
188
|
+
temp.reverse
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Pack table to binary data
|
|
192
|
+
#
|
|
193
|
+
# @return [String] Binary data
|
|
194
|
+
def pack
|
|
195
|
+
table = truncate
|
|
196
|
+
|
|
197
|
+
# Pad to block boundary
|
|
198
|
+
num = @ole.bbat.block_size / 4
|
|
199
|
+
if (table.length % num) != 0
|
|
200
|
+
table += [AVAIL] * (num - (table.length % num))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
table.pack("V*")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Big allocation table (BBAT)
|
|
207
|
+
#
|
|
208
|
+
# Manages large blocks (typically 512 bytes) for files >= 4096 bytes.
|
|
209
|
+
class Big < AllocationTable
|
|
210
|
+
def initialize(ole)
|
|
211
|
+
super
|
|
212
|
+
@block_size = 1 << @ole.header.b_shift
|
|
213
|
+
@io = @ole.io
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Big blocks are -1 based to avoid clashing with header
|
|
217
|
+
#
|
|
218
|
+
# @param blocks [Array<Integer>] Block indices
|
|
219
|
+
# @param size [Integer, nil] Optional size
|
|
220
|
+
# @return [Array<Array<Integer, Integer>>] Byte ranges
|
|
221
|
+
def blocks_to_ranges(blocks, size = nil)
|
|
222
|
+
return [] if blocks.empty?
|
|
223
|
+
|
|
224
|
+
blocks = blocks[0, (size.to_f / block_size).ceil] if size
|
|
225
|
+
ranges = blocks.map { |i| [block_size * (i + 1), block_size] }
|
|
226
|
+
ranges.last[1] -= ((ranges.length * block_size) - size) if ranges.last && size
|
|
227
|
+
ranges
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Small allocation table (SBAT)
|
|
232
|
+
#
|
|
233
|
+
# Manages small blocks (typically 64 bytes) for files < 4096 bytes.
|
|
234
|
+
class Small < AllocationTable
|
|
235
|
+
def initialize(ole)
|
|
236
|
+
super
|
|
237
|
+
@block_size = 1 << @ole.header.s_shift
|
|
238
|
+
@io = @ole.sb_file
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omnizip
|
|
4
|
+
module Formats
|
|
5
|
+
module Ole
|
|
6
|
+
# OLE format constants
|
|
7
|
+
#
|
|
8
|
+
# Constants for the OLE compound document format including
|
|
9
|
+
# magic bytes, special markers, and format-related values.
|
|
10
|
+
module Constants
|
|
11
|
+
# OLE magic signature (D0CF11E0A1B11AE1)
|
|
12
|
+
MAGIC = "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1".b
|
|
13
|
+
|
|
14
|
+
# Header size in bytes
|
|
15
|
+
HEADER_SIZE = 76
|
|
16
|
+
|
|
17
|
+
# Header block size (always 512)
|
|
18
|
+
HEADER_BLOCK_SIZE = 512
|
|
19
|
+
|
|
20
|
+
# Allocation table special markers
|
|
21
|
+
AVAIL = 0xffffffff # Free block
|
|
22
|
+
EOC = 0xfffffffe # End of chain
|
|
23
|
+
BAT = 0xfffffffd # Block stores BAT data
|
|
24
|
+
META_BAT = 0xfffffffc # Block stores Meta BAT
|
|
25
|
+
|
|
26
|
+
# End of tree marker for dirents
|
|
27
|
+
EOT = 0xffffffff
|
|
28
|
+
|
|
29
|
+
# Default threshold for small block vs big block (4096 bytes)
|
|
30
|
+
DEFAULT_THRESHOLD = 4096
|
|
31
|
+
|
|
32
|
+
# Byte order marker for little-endian
|
|
33
|
+
BYTE_ORDER_LE = "\xfe\xff".b
|
|
34
|
+
|
|
35
|
+
# Default block sizes
|
|
36
|
+
DEFAULT_BIG_BLOCK_SHIFT = 9 # 512 bytes
|
|
37
|
+
DEFAULT_SMALL_BLOCK_SHIFT = 6 # 64 bytes
|
|
38
|
+
|
|
39
|
+
# Dirent types
|
|
40
|
+
DIRENT_TYPES = {
|
|
41
|
+
0 => :empty,
|
|
42
|
+
1 => :dir,
|
|
43
|
+
2 => :file,
|
|
44
|
+
5 => :root,
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# Dirent colors for red-black tree
|
|
48
|
+
DIRENT_COLORS = {
|
|
49
|
+
0 => :red,
|
|
50
|
+
1 => :black,
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
# Dirent size in bytes
|
|
54
|
+
DIRENT_SIZE = 128
|
|
55
|
+
|
|
56
|
+
# Maximum name length in UTF-16 characters (including null terminator)
|
|
57
|
+
MAX_NAME_LENGTH = 32
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
require_relative "types/variant"
|
|
5
|
+
|
|
6
|
+
module Omnizip
|
|
7
|
+
module Formats
|
|
8
|
+
module Ole
|
|
9
|
+
# OLE directory entry (dirent)
|
|
10
|
+
#
|
|
11
|
+
# Represents a file or directory entry in an OLE compound document.
|
|
12
|
+
# Each dirent is 128 bytes and contains metadata about the entry.
|
|
13
|
+
class Dirent
|
|
14
|
+
include Constants
|
|
15
|
+
|
|
16
|
+
# Pack format for dirent structure
|
|
17
|
+
PACK = "a64 v C C V3 a16 V a8 a8 V2 a4"
|
|
18
|
+
|
|
19
|
+
# @return [String] 64-byte UTF-16LE name
|
|
20
|
+
attr_accessor :name_utf16
|
|
21
|
+
|
|
22
|
+
# @return [Integer] Name length in bytes
|
|
23
|
+
attr_accessor :name_len
|
|
24
|
+
|
|
25
|
+
# @return [Integer] Entry type (0=empty, 1=dir, 2=file, 5=root)
|
|
26
|
+
attr_accessor :type_id
|
|
27
|
+
|
|
28
|
+
# @return [Integer] Red-black tree color
|
|
29
|
+
attr_accessor :colour
|
|
30
|
+
|
|
31
|
+
# @return [Integer] Previous sibling index
|
|
32
|
+
attr_accessor :prev
|
|
33
|
+
|
|
34
|
+
# @return [Integer] Next sibling index
|
|
35
|
+
attr_accessor :next
|
|
36
|
+
|
|
37
|
+
# @return [Integer] First child index
|
|
38
|
+
attr_accessor :child
|
|
39
|
+
|
|
40
|
+
# @return [String] 16-byte CLSID
|
|
41
|
+
attr_accessor :clsid
|
|
42
|
+
|
|
43
|
+
# @return [Integer] Flags (for directories)
|
|
44
|
+
attr_accessor :flags
|
|
45
|
+
|
|
46
|
+
# @return [String] 8-byte creation time
|
|
47
|
+
attr_accessor :create_time_str
|
|
48
|
+
|
|
49
|
+
# @return [String] 8-byte modification time
|
|
50
|
+
attr_accessor :modify_time_str
|
|
51
|
+
|
|
52
|
+
# @return [Integer] First block of data
|
|
53
|
+
attr_accessor :first_block
|
|
54
|
+
|
|
55
|
+
# @return [Integer] Size in bytes
|
|
56
|
+
attr_accessor :size
|
|
57
|
+
|
|
58
|
+
# @return [String] 4-byte reserved
|
|
59
|
+
attr_accessor :reserved
|
|
60
|
+
|
|
61
|
+
# @return [Object] Parent OLE storage
|
|
62
|
+
attr_reader :ole
|
|
63
|
+
|
|
64
|
+
# @return [Symbol] Entry type (:empty, :dir, :file, :root)
|
|
65
|
+
attr_reader :type
|
|
66
|
+
|
|
67
|
+
# @return [String] Decoded entry name
|
|
68
|
+
attr_reader :name
|
|
69
|
+
|
|
70
|
+
# @return [Time, nil] Creation time
|
|
71
|
+
attr_reader :create_time
|
|
72
|
+
|
|
73
|
+
# @return [Time, nil] Modification time
|
|
74
|
+
attr_reader :modify_time
|
|
75
|
+
|
|
76
|
+
# @return [Array<Dirent>] Child entries (for directories)
|
|
77
|
+
attr_reader :parent
|
|
78
|
+
|
|
79
|
+
# @return [Integer, nil] Index in dirent array (used during loading)
|
|
80
|
+
attr_accessor :idx
|
|
81
|
+
|
|
82
|
+
# Parse dirent from binary data
|
|
83
|
+
#
|
|
84
|
+
# @param ole [Object] Parent OLE storage
|
|
85
|
+
# @param data [String] 128-byte dirent data
|
|
86
|
+
# @return [Dirent] Parsed dirent
|
|
87
|
+
def self.parse(ole, data)
|
|
88
|
+
dirent = new(ole)
|
|
89
|
+
dirent.unpack(data)
|
|
90
|
+
dirent
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create new dirent with specified type and name
|
|
94
|
+
#
|
|
95
|
+
# @param ole [Object] Parent OLE storage
|
|
96
|
+
# @param type [Symbol] Entry type (:file, :dir, :root)
|
|
97
|
+
# @param name [String] Entry name
|
|
98
|
+
# @return [Dirent] New dirent
|
|
99
|
+
def self.create(ole, type:, name:)
|
|
100
|
+
dirent = new(ole)
|
|
101
|
+
dirent.type = type
|
|
102
|
+
dirent.name = name
|
|
103
|
+
dirent
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Initialize dirent
|
|
107
|
+
#
|
|
108
|
+
# @param ole [Object] Parent OLE storage
|
|
109
|
+
def initialize(ole)
|
|
110
|
+
@ole = ole
|
|
111
|
+
@children = []
|
|
112
|
+
@name_lookup = {}
|
|
113
|
+
@parent = nil
|
|
114
|
+
@idx = nil
|
|
115
|
+
apply_defaults
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Apply default values
|
|
119
|
+
def apply_defaults
|
|
120
|
+
@name_utf16 = "\x00".b * 64
|
|
121
|
+
@name_len = 2
|
|
122
|
+
@type_id = 0
|
|
123
|
+
@colour = 1 # black
|
|
124
|
+
@prev = EOT
|
|
125
|
+
@next = EOT
|
|
126
|
+
@child = EOT
|
|
127
|
+
@clsid = "\x00".b * 16
|
|
128
|
+
@flags = 0
|
|
129
|
+
@create_time_str = "\x00".b * 8
|
|
130
|
+
@modify_time_str = "\x00".b * 8
|
|
131
|
+
@first_block = EOC
|
|
132
|
+
@size = 0
|
|
133
|
+
@reserved = "\x00".b * 4
|
|
134
|
+
@type = :empty
|
|
135
|
+
@name = ""
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Set entry type
|
|
139
|
+
#
|
|
140
|
+
# @param value [Symbol] Type (:empty, :dir, :file, :root)
|
|
141
|
+
def type=(value)
|
|
142
|
+
@type = value
|
|
143
|
+
@type_id = DIRENT_TYPES.invert[value] || 0
|
|
144
|
+
|
|
145
|
+
if file?
|
|
146
|
+
@children = nil
|
|
147
|
+
@name_lookup = nil
|
|
148
|
+
else
|
|
149
|
+
@children ||= []
|
|
150
|
+
@name_lookup ||= {}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Set entry name
|
|
155
|
+
#
|
|
156
|
+
# @param value [String] Entry name
|
|
157
|
+
def name=(value)
|
|
158
|
+
if @parent
|
|
159
|
+
map = @parent.instance_variable_get(:@name_lookup)
|
|
160
|
+
map&.delete(@name)
|
|
161
|
+
map&.store(value, self)
|
|
162
|
+
end
|
|
163
|
+
@name = value
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check if entry is a file
|
|
167
|
+
#
|
|
168
|
+
# @return [Boolean]
|
|
169
|
+
def file?
|
|
170
|
+
@type == :file
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check if entry is a directory
|
|
174
|
+
#
|
|
175
|
+
# @return [Boolean]
|
|
176
|
+
def dir?
|
|
177
|
+
!file?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Check if entry is root
|
|
181
|
+
#
|
|
182
|
+
# @return [Boolean]
|
|
183
|
+
def root?
|
|
184
|
+
@type == :root
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Check if entry is empty
|
|
188
|
+
#
|
|
189
|
+
# @return [Boolean]
|
|
190
|
+
def empty?
|
|
191
|
+
@type == :empty
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Unpack dirent from binary data
|
|
195
|
+
#
|
|
196
|
+
# @param data [String] 128-byte dirent data
|
|
197
|
+
def unpack(data)
|
|
198
|
+
values = data.unpack(PACK)
|
|
199
|
+
@name_utf16 = values[0]
|
|
200
|
+
@name_len = values[1]
|
|
201
|
+
@type_id = values[2]
|
|
202
|
+
@colour = values[3]
|
|
203
|
+
@prev = values[4]
|
|
204
|
+
@next = values[5]
|
|
205
|
+
@child = values[6]
|
|
206
|
+
@clsid = values[7]
|
|
207
|
+
@flags = values[8]
|
|
208
|
+
@create_time_str = values[9]
|
|
209
|
+
@modify_time_str = values[10]
|
|
210
|
+
@first_block = values[11]
|
|
211
|
+
@size = values[12]
|
|
212
|
+
@reserved = values[13]
|
|
213
|
+
|
|
214
|
+
# Decode name from UTF-16LE
|
|
215
|
+
name_data = @name_utf16[0...@name_len]
|
|
216
|
+
@name = begin
|
|
217
|
+
Types::Variant.load(Types::Variant::VT_LPWSTR, name_data)
|
|
218
|
+
rescue StandardError
|
|
219
|
+
""
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Decode type
|
|
223
|
+
@type = DIRENT_TYPES[@type_id] || :empty
|
|
224
|
+
|
|
225
|
+
# Decode timestamps for files
|
|
226
|
+
if file?
|
|
227
|
+
@create_time = begin
|
|
228
|
+
Types::Variant.load(Types::Variant::VT_FILETIME, @create_time_str)
|
|
229
|
+
rescue StandardError
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
@modify_time = begin
|
|
233
|
+
Types::Variant.load(Types::Variant::VT_FILETIME, @modify_time_str)
|
|
234
|
+
rescue StandardError
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
@children = nil
|
|
238
|
+
@name_lookup = nil
|
|
239
|
+
else
|
|
240
|
+
@create_time = nil
|
|
241
|
+
@modify_time = nil
|
|
242
|
+
@children = []
|
|
243
|
+
@name_lookup = {}
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Pack dirent to binary data
|
|
248
|
+
#
|
|
249
|
+
# @return [String] 128-byte binary data
|
|
250
|
+
def pack
|
|
251
|
+
# Encode name to UTF-16LE
|
|
252
|
+
name_data = Types::Variant.dump(Types::Variant::VT_LPWSTR, @name)
|
|
253
|
+
name_data = name_data[0, 62] if name_data.length > 62
|
|
254
|
+
name_data += "\x00\x00".b
|
|
255
|
+
@name_len = name_data.length
|
|
256
|
+
@name_utf16 = name_data + ("\x00".b * (64 - name_data.length))
|
|
257
|
+
|
|
258
|
+
# Set type_id from type
|
|
259
|
+
@type_id = DIRENT_TYPES.invert[@type] || 0
|
|
260
|
+
|
|
261
|
+
# For directories, first_block should be EOT
|
|
262
|
+
if dir? && !root?
|
|
263
|
+
@first_block = EOT
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Encode timestamps for files
|
|
267
|
+
if file?
|
|
268
|
+
@create_time_str = Types::Variant.dump(Types::Variant::VT_FILETIME, @create_time) if @create_time
|
|
269
|
+
@modify_time_str = Types::Variant.dump(Types::Variant::VT_FILETIME, @modify_time) if @modify_time
|
|
270
|
+
else
|
|
271
|
+
@create_time_str = "\x00".b * 8
|
|
272
|
+
@modify_time_str = "\x00".b * 8
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
[
|
|
276
|
+
@name_utf16, @name_len, @type_id, @colour,
|
|
277
|
+
@prev, @next, @child, @clsid, @flags,
|
|
278
|
+
@create_time_str, @modify_time_str,
|
|
279
|
+
@first_block, @size, @reserved
|
|
280
|
+
].pack(PACK)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Read file content
|
|
284
|
+
#
|
|
285
|
+
# @return [String] File content
|
|
286
|
+
# @raise [Errno::EISDIR] If entry is a directory
|
|
287
|
+
def read
|
|
288
|
+
raise Errno::EISDIR unless file?
|
|
289
|
+
|
|
290
|
+
bat = @ole.bat_for_size(@size)
|
|
291
|
+
bat.read(@first_block, @size)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Lookup child by name
|
|
295
|
+
#
|
|
296
|
+
# @param name [String] Child name
|
|
297
|
+
# @return [Dirent, nil] Child dirent
|
|
298
|
+
def /(name)
|
|
299
|
+
@name_lookup&.[](name)
|
|
300
|
+
end
|
|
301
|
+
alias [] :/
|
|
302
|
+
|
|
303
|
+
# Add child entry
|
|
304
|
+
#
|
|
305
|
+
# @param child [Dirent] Child to add
|
|
306
|
+
def <<(child)
|
|
307
|
+
child.parent = self
|
|
308
|
+
@name_lookup[child.name] = child if @name_lookup
|
|
309
|
+
@children << child
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Set parent entry
|
|
313
|
+
#
|
|
314
|
+
# @param parent [Dirent, nil] Parent dirent
|
|
315
|
+
def parent=(parent)
|
|
316
|
+
@parent = parent
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Get all children
|
|
320
|
+
#
|
|
321
|
+
# @return [Array<Dirent>]
|
|
322
|
+
def children
|
|
323
|
+
@children || []
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Iterate over children
|
|
327
|
+
#
|
|
328
|
+
# @yield [Dirent]
|
|
329
|
+
def each_child(&block)
|
|
330
|
+
@children&.each(&block)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Flatten tree to array for serialization
|
|
334
|
+
#
|
|
335
|
+
# @param dirents [Array<Dirent>] Output array
|
|
336
|
+
# @return [Array<Dirent>]
|
|
337
|
+
def flatten(dirents = [])
|
|
338
|
+
@idx = dirents.length
|
|
339
|
+
dirents << self
|
|
340
|
+
|
|
341
|
+
if file?
|
|
342
|
+
self.prev = EOT
|
|
343
|
+
self.next = EOT
|
|
344
|
+
self.child = EOT
|
|
345
|
+
else
|
|
346
|
+
children.each { |child| child.flatten(dirents) }
|
|
347
|
+
self.child = self.class.flatten_helper(children)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
dirents
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Helper to create balanced tree structure
|
|
354
|
+
#
|
|
355
|
+
# @param children [Array<Dirent>]
|
|
356
|
+
# @return [Integer] Index of root of subtree
|
|
357
|
+
def self.flatten_helper(children)
|
|
358
|
+
return EOT if children.empty?
|
|
359
|
+
|
|
360
|
+
i = children.length / 2
|
|
361
|
+
this = children[i]
|
|
362
|
+
this.prev = flatten_helper(children[0...i])
|
|
363
|
+
this.next = flatten_helper(children[(i + 1)..])
|
|
364
|
+
this.idx
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Inspect dirent
|
|
368
|
+
#
|
|
369
|
+
# @return [String]
|
|
370
|
+
def inspect
|
|
371
|
+
str = "#<Ole::Dirent:#{@name.inspect}"
|
|
372
|
+
if file?
|
|
373
|
+
str << " size=#{@size}"
|
|
374
|
+
end
|
|
375
|
+
"#{str}>"
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|