File: property_set.rb

package info (click to toggle)
ruby-ole 1.2.11.3-1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 628 kB
  • sloc: ruby: 2,870; makefile: 9
file content (173 lines) | stat: -rw-r--r-- 4,726 bytes parent folder | download
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