Class: Omnizip::Formats::Ole::Storage

Inherits:
Object
  • Object
show all
Includes:
Constants
Defined in:
lib/omnizip/formats/ole/storage.rb

Overview

OLE compound document storage

Main class for reading and writing OLE compound documents. Provides access to the hierarchical file structure within the document.

Defined Under Namespace

Classes: FormatError

Constant Summary

Constants included from Constants

Constants::AVAIL, Constants::BAT, Constants::BYTE_ORDER_LE, Constants::DEFAULT_BIG_BLOCK_SHIFT, Constants::DEFAULT_SMALL_BLOCK_SHIFT, Constants::DEFAULT_THRESHOLD, Constants::DIRENT_COLORS, Constants::DIRENT_SIZE, Constants::DIRENT_TYPES, Constants::EOC, Constants::EOT, Constants::HEADER_BLOCK_SIZE, Constants::HEADER_SIZE, Constants::MAGIC, Constants::MAX_NAME_LENGTH, Constants::META_BAT

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path_or_io, mode = nil) ⇒ Storage

Initialize storage

Parameters:

  • path_or_io (String, IO)

    File path or IO object

  • mode (String, nil) (defaults to: nil)

    Open mode



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/omnizip/formats/ole/storage.rb', line 68

def initialize(path_or_io, mode = nil)
  @close_parent, @io = if path_or_io.is_a?(String)
                         mode ||= "rb"
                         [true, File.open(path_or_io, mode)]
                       else
                         if mode
                           raise ArgumentError,
                                 "Cannot specify mode with IO object"
                         end

                         [false, path_or_io]
                       end

  # Force binary encoding
  @io.set_encoding(Encoding::ASCII_8BIT) if @io.is_a?(::IO)

  # Determine if writable
  @writeable = determine_writeable(mode)

  @sb_file = nil

  # Load or create
  if @io.size.positive?
    load
  else
    create_empty
  end
end

Instance Attribute Details

#bbatAllocationTable::Big (readonly)

Returns Big block allocation table.

Returns:



31
32
33
# File 'lib/omnizip/formats/ole/storage.rb', line 31

def bbat
  @bbat
end

#close_parentBoolean (readonly)

Returns Whether to close IO on #close.

Returns:

  • (Boolean)

    Whether to close IO on #close



22
23
24
# File 'lib/omnizip/formats/ole/storage.rb', line 22

def close_parent
  @close_parent
end

#direntsArray<Dirent> (readonly)

Returns All dirents (flat list).

Returns:

  • (Array<Dirent>)

    All dirents (flat list)



43
44
45
# File 'lib/omnizip/formats/ole/storage.rb', line 43

def dirents
  @dirents
end

#headerHeader (readonly)

Returns Parsed header.

Returns:



28
29
30
# File 'lib/omnizip/formats/ole/storage.rb', line 28

def header
  @header
end

#ioIO (readonly)

Returns Underlying IO object.

Returns:

  • (IO)

    Underlying IO object



19
20
21
# File 'lib/omnizip/formats/ole/storage.rb', line 19

def io
  @io
end

#rootDirent (readonly)

Returns Root entry.

Returns:



40
41
42
# File 'lib/omnizip/formats/ole/storage.rb', line 40

def root
  @root
end

#sb_fileRangesIO (readonly)

Returns Small block file.

Returns:



37
38
39
# File 'lib/omnizip/formats/ole/storage.rb', line 37

def sb_file
  @sb_file
end

#sbatAllocationTable::Small (readonly)

Returns Small block allocation table.

Returns:



34
35
36
# File 'lib/omnizip/formats/ole/storage.rb', line 34

def sbat
  @sbat
end

#writeableBoolean (readonly)

Returns Whether opened for writing.

Returns:

  • (Boolean)

    Whether opened for writing



25
26
27
# File 'lib/omnizip/formats/ole/storage.rb', line 25

def writeable
  @writeable
end

Class Method Details

.open(path_or_io, mode = nil) {|Storage| ... } ⇒ Storage

Open OLE file

Parameters:

  • path_or_io (String, IO)

    File path or IO object

  • mode (String, nil) (defaults to: nil)

    Open mode

Yields:

Returns:



51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/omnizip/formats/ole/storage.rb', line 51

def self.open(path_or_io, mode = nil)
  storage = new(path_or_io, mode)
  if block_given?
    begin
      yield storage
    ensure
      storage.close
    end
  else
    storage
  end
end

Instance Method Details

#bat_for_size(size) ⇒ AllocationTable

Get appropriate BAT for size

Parameters:

  • size (Integer)

    File size

Returns:



348
349
350
# File 'lib/omnizip/formats/ole/storage.rb', line 348

def bat_for_size(size)
  size >= @header.threshold ? @bbat : @sbat
end

#build_tree(dirents, idx = 0, visited = nil) ⇒ Array<Dirent>

Build tree from flat dirent list

Parameters:

  • dirents (Array<Dirent>)
  • idx (Integer) (defaults to: 0)
  • visited (Set) (defaults to: nil)

    Set of visited indices to detect cycles

Returns:



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/omnizip/formats/ole/storage.rb', line 147

def build_tree(dirents, idx = 0, visited = nil)
  return [] if idx == EOT

  # Initialize visited set on first call
  visited ||= Set.new

  # Check for circular references
  return [] if visited.include?(idx)

  visited << idx

  dirent = dirents[idx]
  return [] unless dirent

  # Build children recursively
  build_tree(dirents, dirent.child, visited).each do |child|
    dirent << child
  end

  # Set index
  dirent.idx = idx

  # Return list for tree building
  build_tree(dirents, dirent.prev,
             visited) + [dirent] + build_tree(dirents, dirent.next,
                                              visited)
end

#closeObject

Close storage



176
177
178
179
180
# File 'lib/omnizip/formats/ole/storage.rb', line 176

def close
  flush if @writeable
  free
  @io.close if @close_parent
end

#directory?(path) ⇒ Boolean

Check if path is a directory

Parameters:

  • path (String)

Returns:

  • (Boolean)


398
399
400
401
# File 'lib/omnizip/formats/ole/storage.rb', line 398

def directory?(path)
  dirent = find_dirent(path)
  dirent&.dir?
end

#exist?(path) ⇒ Boolean Also known as: exists?

Check if entry exists

Parameters:

  • path (String)

Returns:

  • (Boolean)


379
380
381
# File 'lib/omnizip/formats/ole/storage.rb', line 379

def exist?(path)
  !find_dirent(path).nil?
end

#file?(path) ⇒ Boolean

Check if path is a file

Parameters:

  • path (String)

Returns:

  • (Boolean)


389
390
391
392
# File 'lib/omnizip/formats/ole/storage.rb', line 389

def file?(path)
  dirent = find_dirent(path)
  dirent&.file?
end

#find_dirent(path) ⇒ Dirent?

Find dirent by path

Parameters:

  • path (String)

Returns:



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/omnizip/formats/ole/storage.rb', line 424

def find_dirent(path)
  path = path.to_s
  path = path[1..] if path.start_with?("/")

  return @root if path.empty?

  parts = path.split("/")
  current = @root

  parts.each do |part|
    next if part.empty?
    return nil if current.file?

    current = current / part
    return nil unless current
  end

  current
end

#flushObject

Flush all changes to disk

Writes all metadata (dirents, allocation tables, header) to the file. This is the main "save" method for OLE documents.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/omnizip/formats/ole/storage.rb', line 202

def flush
  return unless @writeable

  # Update root dirent
  @root.name = "Root Entry"
  @root.first_block = @sb_file.first_block
  @root.size = @sb_file.size

  # Flatten dirent tree
  @dirents = @root.flatten

  # Serialize dirents using bbat
  dirent_io = RangesIOResizeable.new(@bbat,
                                     first_block: @header.dirent_start)
  dirent_io.write(@dirents.map(&:pack).join)
  # Pad to block boundary
  padding = ((dirent_io.size / @bbat.block_size.to_f).ceil * @bbat.block_size) - dirent_io.size
  dirent_io.write("\x00".b * padding) if padding.positive?
  @header.dirent_start = dirent_io.first_block
  dirent_io.close

  # Serialize sbat
  sbat_io = RangesIOResizeable.new(@bbat,
                                   first_block: @header.sbat_start)
  sbat_io.write(@sbat.pack)
  @header.sbat_start = sbat_io.first_block
  @header.num_sbat = @bbat.chain(@header.sbat_start).length
  sbat_io.close

  # Clear BAT/META_BAT markers
  @bbat.entries.each_with_index do |val, idx|
    if [BAT, META_BAT].include?(val)
      @bbat.entries[idx] = AVAIL
    end
  end

  # Calculate and allocate BAT blocks
  write_bat_blocks

  # Write header
  @io.seek(0)
  @io.write(@header.pack)
  @io.write(@bbat_chain.pack("V*"))
  @io.flush
end

#freevoid

This method returns an undefined value.

Free internal resources to prevent memory leaks

Call this to release memory while keeping the IO open.



186
187
188
189
190
191
192
193
194
195
196
# File 'lib/omnizip/formats/ole/storage.rb', line 186

def free
  @sb_file&.free
  @sb_file = nil
  @sbat&.free
  @sbat = nil
  @bbat&.free
  @bbat = nil
  @dirents = nil
  @root = nil
  @header = nil
end

#info(path) ⇒ Hash?

Get entry info

Parameters:

  • path (String)

Returns:

  • (Hash, nil)


407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/omnizip/formats/ole/storage.rb', line 407

def info(path)
  dirent = find_dirent(path)
  return nil unless dirent

  {
    name: dirent.name,
    type: dirent.type,
    size: dirent.size,
    create_time: dirent.create_time,
    modify_time: dirent.modify_time,
  }
end

#inspectObject

Inspect



445
446
447
# File 'lib/omnizip/formats/ole/storage.rb', line 445

def inspect
  "#<#{self.class} io=#{@io.inspect} root=#{@root.inspect}>"
end

#list(path = "/") ⇒ Array<String>

List entries at path

Parameters:

  • path (String) (defaults to: "/")

    Directory path

Returns:

  • (Array<String>)


356
357
358
359
360
361
# File 'lib/omnizip/formats/ole/storage.rb', line 356

def list(path = "/")
  dirent = find_dirent(path)
  return [] unless dirent

  dirent.children.map(&:name)
end

#loadObject

Load OLE document from IO



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/omnizip/formats/ole/storage.rb', line 98

def load
  @io.rewind
  header_block = @io.read(HEADER_BLOCK_SIZE)

  # Parse header
  @header = Header.parse(header_block)

  # Build BBAT chain from header
  @bbat = AllocationTable::Big.new(self)
  bbat_chain = header_block[HEADER_SIZE..].unpack("V*")

  # Add Meta BAT blocks if present
  mbat_block = @header.mbat_start
  @header.num_mbat.times do
    blocks = @bbat.read([mbat_block]).unpack("V*")
    mbat_block = blocks.pop
    bbat_chain += blocks
  end

  # Load BBAT
  @bbat.load(@bbat.read(bbat_chain[0, @header.num_bat]))

  # Load dirents
  raw_dirents = @bbat.read(@header.dirent_start)
  @dirents = []
  (raw_dirents.bytesize / DIRENT_SIZE).times do |i|
    dirent_data = raw_dirents.byteslice(i * DIRENT_SIZE, DIRENT_SIZE)
    @dirents << Dirent.parse(self, dirent_data)
  end

  # Build tree structure
  @root = build_tree(@dirents).first

  # Remove empty entries
  @dirents.reject!(&:empty?)

  # Setup SBAT
  @sb_file = RangesIOResizeable.new(@bbat,
                                    first_block: @root.first_block, size: @root.size)
  @sbat = AllocationTable::Small.new(self)
  @sbat.load(@bbat.read(@header.sbat_start))
end

#read(path) ⇒ String

Read file content

Parameters:

  • path (String)

    File path

Returns:

  • (String)

Raises:

  • (Errno::ENOENT)


367
368
369
370
371
372
373
# File 'lib/omnizip/formats/ole/storage.rb', line 367

def read(path)
  dirent = find_dirent(path)
  raise Errno::ENOENT, path unless dirent
  raise Errno::EISDIR, path if dirent.dir?

  dirent.read
end