Class: Omnizip::Formats::Ole::Storage
- Inherits:
-
Object
- Object
- Omnizip::Formats::Ole::Storage
- 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
-
#bbat ⇒ AllocationTable::Big
readonly
Big block allocation table.
-
#close_parent ⇒ Boolean
readonly
Whether to close IO on #close.
-
#dirents ⇒ Array<Dirent>
readonly
All dirents (flat list).
-
#header ⇒ Header
readonly
Parsed header.
-
#io ⇒ IO
readonly
Underlying IO object.
-
#root ⇒ Dirent
readonly
Root entry.
-
#sb_file ⇒ RangesIO
readonly
Small block file.
-
#sbat ⇒ AllocationTable::Small
readonly
Small block allocation table.
-
#writeable ⇒ Boolean
readonly
Whether opened for writing.
Class Method Summary collapse
-
.open(path_or_io, mode = nil) {|Storage| ... } ⇒ Storage
Open OLE file.
Instance Method Summary collapse
-
#bat_for_size(size) ⇒ AllocationTable
Get appropriate BAT for size.
-
#build_tree(dirents, idx = 0, visited = nil) ⇒ Array<Dirent>
Build tree from flat dirent list.
-
#close ⇒ Object
Close storage.
-
#directory?(path) ⇒ Boolean
Check if path is a directory.
-
#exist?(path) ⇒ Boolean
(also: #exists?)
Check if entry exists.
-
#file?(path) ⇒ Boolean
Check if path is a file.
-
#find_dirent(path) ⇒ Dirent?
Find dirent by path.
-
#flush ⇒ Object
Flush all changes to disk.
-
#free ⇒ void
Free internal resources to prevent memory leaks.
-
#info(path) ⇒ Hash?
Get entry info.
-
#initialize(path_or_io, mode = nil) ⇒ Storage
constructor
Initialize storage.
-
#inspect ⇒ Object
Inspect.
-
#list(path = "/") ⇒ Array<String>
List entries at path.
-
#load ⇒ Object
Load OLE document from IO.
-
#read(path) ⇒ String
Read file content.
Constructor Details
#initialize(path_or_io, mode = nil) ⇒ Storage
Initialize storage
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
#bbat ⇒ AllocationTable::Big (readonly)
Returns Big block allocation table.
31 32 33 |
# File 'lib/omnizip/formats/ole/storage.rb', line 31 def bbat @bbat end |
#close_parent ⇒ Boolean (readonly)
Returns Whether to close IO on #close.
22 23 24 |
# File 'lib/omnizip/formats/ole/storage.rb', line 22 def close_parent @close_parent end |
#dirents ⇒ Array<Dirent> (readonly)
Returns All dirents (flat list).
43 44 45 |
# File 'lib/omnizip/formats/ole/storage.rb', line 43 def dirents @dirents end |
#header ⇒ Header (readonly)
Returns Parsed header.
28 29 30 |
# File 'lib/omnizip/formats/ole/storage.rb', line 28 def header @header end |
#io ⇒ IO (readonly)
Returns Underlying IO object.
19 20 21 |
# File 'lib/omnizip/formats/ole/storage.rb', line 19 def io @io end |
#root ⇒ Dirent (readonly)
Returns Root entry.
40 41 42 |
# File 'lib/omnizip/formats/ole/storage.rb', line 40 def root @root end |
#sb_file ⇒ RangesIO (readonly)
Returns Small block file.
37 38 39 |
# File 'lib/omnizip/formats/ole/storage.rb', line 37 def sb_file @sb_file end |
#sbat ⇒ AllocationTable::Small (readonly)
Returns Small block allocation table.
34 35 36 |
# File 'lib/omnizip/formats/ole/storage.rb', line 34 def sbat @sbat end |
#writeable ⇒ Boolean (readonly)
Returns 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
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
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
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 |
#close ⇒ Object
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
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
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
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
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 |
#flush ⇒ Object
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 |
#free ⇒ void
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
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 |
#inspect ⇒ Object
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
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 |
#load ⇒ Object
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
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 |