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,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