# -*- coding: binary -*- ## # Rex::OLE - an OLE implementation # written in 2010 by Joshua J. Drake ## module Rex module OLE class Storage attr_accessor :header def initialize(filename=nil, mode=STGM_READ) @mode = mode @modified = nil @fd = nil @filename = nil @header = Header.new @difat = DIFAT.new self @fat = FAT.new self @minifat = MiniFAT.new self @directory = Directory.new self @ministream = Stream.new self if (filename) @filename = filename open(filename, mode) return end end def each @directory.each { |el| yield el } end def name @filename end def open(filename, mode) if (mode == STGM_READWRITE) fmode = 'r+b' elsif (mode == STGM_WRITE) fmode = 'w+b' else fmode = 'rb' end @fd = File.new(filename, fmode) # don't read for new files if (mode == STGM_WRITE) # ensure there is a root write_to_disk return end # parse the header @header.read @fd @difat.read @fat.read @difat @minifat.read @directory.read # NOTE: we can't use read_stream_data here (must read using regular FAT, regardless of size) # read data using the root node's start/length @ministream << read_data(@directory) end def close if (@modified) and (@mode != STGM_READ) write_to_disk end @fd.close end def inspect ret = "" ret << "header = %s\n" % @header.to_s ret << "*** %u DIFAT sectors\n" % @difat.length ret << @difat.to_s << "\n" ret << "*** %u FAT sectors\n" % @fat.length ret << @fat.to_s << "\n" ret << "*** %u MiniFAT sectors:\n" % @minifat.length if (@minifat.length > 0) ret << @minifat.to_s << "\n" end ret << "*** ministream (%u bytes):\n" % @ministream.length if (@ministream.length > 0) ret << @ministream.to_s << "\n" end ret << "*** %u directory entries\n" % @directory.num_entries ret << @directory.to_s << "\n" end # # stream manipulation functions # def create_stream(name, mode=STGM_WRITE, parent_stg=nil) if (stm = open_stream(name, mode, parent_stg)) stm.close return nil end # eek, don't check the name for now # if we do, we cant create alot of streams (summary info for example) =begin if (not Util.name_is_valid(name)) return nil end =end stm = Stream.new self stm.name = name parent_stg ||= @directory dlog("Adding stream #{name} to storage #{parent_stg.name}", 'rex', LEV_3) @directory.link_item(parent_stg, stm) @modified = true stm end def open_stream(name, mode=STGM_READ, parent_stg=nil) parent_stg ||= @directory stm = parent_stg.find_stream_by_name_and_type(name, STGTY_STREAM) if (stm) # TODO: optimize out the need to read all of the data up-front stm << read_stream_data(stm) end stm end # # storage manipulation functions # def create_storage(name, mode=STGM_READ, parent_stg=nil) stg = SubStorage.new self stg.name = name parent_stg ||= @directory dlog("Adding storage #{name} to storage #{parent_stg.name}", 'rex', LEV_3) @directory.link_item(parent_stg, stg) stg end def open_storage(name, mode=STGM_READ, parent_stg=nil) @directory.find_stream_by_name_and_type(name, STGTY_STORAGE) end # # low-level functions # def write_to_disk # reset FAT/DIFAT @difat = DIFAT.new self @fat = FAT.new self @header.write @fd write_user_data # NOTE: we call write_stream here since we MUST write this to # the regular stream (regardless of size) ms_start = write_stream(@ministream) @directory.set_ministream_params(ms_start, @ministream.length) @minifat.write @directory.write @fat.write(@difat) @difat.write # write it again, now that its complete @header.write @fd @fd.flush end def write_sector(sbuf, type=nil, prev_sect=nil) len = sbuf.length if (len != @header.sector_size) # pad it if less if (len < @header.sector_size) sbuf = sbuf.dup sbuf << "\x00" * (@header.sector_size - len) else raise RuntimeError, 'not sector sized!' end end # write the data idx = @fat.allocate_sector(type) # point previous sector to here if (prev_sect) @fat[prev_sect] = idx end write_sector_raw(idx, sbuf) return idx end def write_sector_raw(sect, sbuf) dlog("Writing sector 0x%02x" % sect, 'rex', LEV_3) @fd.seek((sect + 1) * @header.sector_size, ::IO::SEEK_SET) @fd.write(sbuf) end def write_mini_sector(sbuf, prev_sect=nil) len = sbuf.length if (len != @header.mini_sector_size) if (len < @header.mini_sector_size) sbuf = sbuf.dup sbuf << "\x00" * (@header.mini_sector_size - len) else raise RuntimeError, 'not mini sector sized!' end end idx = @minifat.allocate_sector # point the previous mini sector to here if (prev_sect) @minifat[prev_sect] = idx end write_mini_sector_raw(idx, sbuf) idx end def write_mini_sector_raw(sect, sbuf) dlog("Writing mini sector 0x%02x" % sect, 'rex', LEV_3) @ministream << sbuf end def write_user_data @directory.each_entry { |stm| # only regular streams this pass next if (stm.type != STGTY_STREAM) if (stm.length >= @header._ulMiniSectorCutoff) stm.start_sector = write_stream(stm) else # NOTE: stm_start is a minifat value stm.start_sector = write_mini_stream(stm) end } end def write_stream(stm) dlog("Writing \"%s\" to regular stream" % stm.name, 'rex', LEV_3) stm_start = nil prev_sect = nil stm.seek(0) while (sbuf = stm.read(@header.sector_size)) sect = write_sector(sbuf, nil, prev_sect) stm_start ||= sect prev_sect = sect end stm_start end def write_mini_stream(stm) dlog("Writing \"%s\" to mini stream" % stm.name, 'rex', LEV_3) prev_sect = nil stm.seek(0) while (sbuf = stm.read(@header.mini_sector_size)) sect = write_mini_sector(sbuf, prev_sect) stm_start ||= sect prev_sect = sect end stm_start end def read_stream_data(direntry) if (direntry.length < @header._ulMiniSectorCutoff) return read_data_mini(direntry) end read_data(direntry) end def read_data(direntry) ret = "" visited = [] left = direntry.length sect = direntry.start_sector while (sect != SECT_END) if (visited.include?(sect)) raise RuntimeError, 'Sector chain loop detected (0x%08x)' % sect end visited << sect # how much to read? block = @header.sector_size block = left if (block > left) # read it. dlog("read_data - reading 0x%x bytes" % block, 'rex', LEV_3) buf = read_sector(sect, block) ret << buf left -= buf.length # done? break if (left == 0) sect = next_sector(sect) end ret end def read_data_mini(direntry) ret = "" visited = [] left = direntry.length sect = direntry.start_sector while (sect != SECT_END) if (visited.include?(sect)) raise RuntimeError, 'Sector chain loop detected (0x%08x mini)' % sect end visited << sect # how much to read? block = @header.mini_sector_size block = left if (block > left) # read it. dlog("read_data_mini - reading 0x%x bytes" % block, 'rex', LEV_3) buf = read_mini_sector(sect, block) ret << buf left -= buf.length # done? break if (left == 0) sect = next_mini_sector(sect) end ret end def read_sector(sect, len) off = ((sect + 1) * @header.sector_size) @fd.seek(off, ::IO::SEEK_SET) buf = @fd.read(len) if (not buf) if (@fd.eof?) raise RuntimeError, 'EOF while reading sector data (0x%08x)' % sect else raise RuntimeError, 'Unknown error while reading sector data (0x%08x)' % sect end end if (buf.length != len) raise RuntimeError, 'Insufficient data for sector (0x%08x): got %u of %u' % [sect, buf.length, len] end buf end def next_sector(sect) return SECT_END if (sect >= @fat.length) @fat[sect] end def read_mini_sector(sect, len) dlog("Reading mini sector 0x%x" % sect, 'rex', LEV_3) off = (@header.mini_sector_size * sect) dlog("Reading from offset 0x%x of ministream" % off, 'rex', LEV_3) @ministream.seek(off) data = @ministream.read(len) data end def next_mini_sector(sect) return SECT_END if (sect >= @minifat.length) @minifat[sect] end end end end