1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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 96 97 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 140 141 142 143 144 145 146 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
|
# encoding: ASCII-8BIT
require 'yaml'
module Ole
module Types
#
# The PropertySet class currently supports readonly access to the properties
# serialized in "property set" streams, such as the file "\005SummaryInformation",
# in OLE files.
#
# Think it has its roots in MFC property set serialization.
#
# See http://poi.apache.org/hpsf/internals.html for details
#
class PropertySet
HEADER_SIZE = 28
HEADER_PACK = "vvVa#{Clsid::SIZE}V"
OS_MAP = {
0 => :win16,
1 => :mac,
2 => :win32,
0x20001 => :ooffice, # open office on linux...
}
# define a smattering of the property set guids.
propids = [ '/usr/share/ruby-ole/propids.yaml',
File.dirname(__FILE__) + '/../../../../../share/ruby-ole/data/propids.yaml',
File.dirname(__FILE__) + '/../../../data/propids.yaml'
].select { |filepath|
File.exists?(filepath) }.first
raise Exception, 'propids.yaml file could not be found' if propids.nil?
DATA = YAML.load_file(propids).
inject({}) { |hash, (key, value)| hash.update Clsid.parse(key) => value }
# create an inverted map of names to guid/key pairs
PROPERTY_MAP = DATA.inject({}) do |h1, (guid, data)|
data[1].inject(h1) { |h2, (id, name)| h2.update name => [guid, id] }
end
module Constants
DATA.each { |guid, (name, map)| const_set name, guid }
end
include Constants
include Enumerable
class Section
include Variant::Constants
include Enumerable
SIZE = Clsid::SIZE + 4
PACK = "a#{Clsid::SIZE}v"
attr_accessor :guid, :offset
attr_reader :length
def initialize str, property_set
@property_set = property_set
@guid, @offset = str.unpack PACK
self.guid = Clsid.load guid
load_header
end
def io
@property_set.io
end
def load_header
io.seek offset
@byte_size, @length = io.read(8).unpack 'V2'
end
def [] key
each_raw do |id, property_offset|
return read_property(property_offset).last if key == id
end
nil
end
def []= key, value
raise NotImplementedError, 'section writes not yet implemented'
end
def each
each_raw do |id, property_offset|
yield id, read_property(property_offset).last
end
end
private
def each_raw
io.seek offset + 8
io.read(length * 8).each_chunk(8) { |str| yield(*str.unpack('V2')) }
end
def read_property property_offset
io.seek offset + property_offset
type, value = io.read(8).unpack('V2')
# is the method of serialization here custom?
case type
when VT_LPSTR, VT_LPWSTR
value = Variant.load type, io.read(value)
# ....
end
[type, value]
end
end
attr_reader :io, :signature, :unknown, :os, :guid, :sections
def initialize io
@io = io
load_header io.read(HEADER_SIZE)
load_section_list io.read(@num_sections * Section::SIZE)
# expect no gap between last section and start of data.
#Log.warn "gap between section list and property data" unless io.pos == @sections.map(&:offset).min
end
def load_header str
@signature, @unknown, @os_id, @guid, @num_sections = str.unpack HEADER_PACK
# should i check that unknown == 0? it usually is. so is the guid actually
@guid = Clsid.load @guid
@os = OS_MAP[@os_id] || Log.warn("unknown operating system id #{@os_id}")
end
def load_section_list str
@sections = str.to_enum(:each_chunk, Section::SIZE).map { |s| Section.new s, self }
end
def [] key
pair = PROPERTY_MAP[key.to_s] or return nil
section = @sections.find { |s| s.guid == pair.first } or return nil
section[pair.last]
end
def []= key, value
pair = PROPERTY_MAP[key.to_s] or return nil
section = @sections.find { |s| s.guid == pair.first } or return nil
section[pair.last] = value
end
def method_missing name, *args, &block
if name.to_s =~ /(.*)=$/
return super unless args.length == 1
return super unless PROPERTY_MAP[$1]
self[$1] = args.first
else
return super unless args.length == 0
return super unless PROPERTY_MAP[name.to_s]
self[name]
end
end
def each
@sections.each do |section|
next unless pair = DATA[section.guid]
map = pair.last
section.each do |id, value|
name = map[id] or next
yield name, value
end
end
end
def to_h
inject({}) { |hash, (name, value)| hash.update name.to_sym => value }
end
end
end
end
|