ruby-ole 1.2.6 → 1.2.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,165 @@
1
+ require 'ole/types'
2
+ require 'yaml'
3
+
4
+ module Ole
5
+ module Types
6
+ #
7
+ # The PropertySet class currently supports readonly access to the properties
8
+ # serialized in "property set" streams, such as the file "\005SummaryInformation",
9
+ # in OLE files.
10
+ #
11
+ # Think it has its roots in MFC property set serialization.
12
+ #
13
+ # See http://poi.apache.org/hpsf/internals.html for details
14
+ #
15
+ class PropertySet
16
+ HEADER_SIZE = 28
17
+ HEADER_PACK = "vvVa#{Clsid::SIZE}V"
18
+ OS_MAP = {
19
+ 0 => :win16,
20
+ 1 => :mac,
21
+ 2 => :win32,
22
+ 0x20001 => :ooffice, # open office on linux...
23
+ }
24
+
25
+ # define a smattering of the property set guids.
26
+ DATA = YAML.load_file(File.dirname(__FILE__) + '/../../../data/propids.yaml').
27
+ inject({}) { |hash, (key, value)| hash.update Clsid.parse(key) => value }
28
+
29
+ # create an inverted map of names to guid/key pairs
30
+ PROPERTY_MAP = DATA.inject({}) do |h1, (guid, data)|
31
+ data[1].inject(h1) { |h2, (id, name)| h2.update name => [guid, id] }
32
+ end
33
+
34
+ module Constants
35
+ DATA.each { |guid, (name, map)| const_set name, guid }
36
+ end
37
+
38
+ include Constants
39
+ include Enumerable
40
+
41
+ class Section
42
+ include Variant::Constants
43
+ include Enumerable
44
+
45
+ SIZE = Clsid::SIZE + 4
46
+ PACK = "a#{Clsid::SIZE}v"
47
+
48
+ attr_accessor :guid, :offset
49
+ attr_reader :length
50
+
51
+ def initialize str, property_set
52
+ @property_set = property_set
53
+ @guid, @offset = str.unpack PACK
54
+ self.guid = Clsid.load guid
55
+ load_header
56
+ end
57
+
58
+ def io
59
+ @property_set.io
60
+ end
61
+
62
+ def load_header
63
+ io.seek offset
64
+ @byte_size, @length = io.read(8).unpack 'V2'
65
+ end
66
+
67
+ def [] key
68
+ each_raw do |id, property_offset|
69
+ return read_property(property_offset).last if key == id
70
+ end
71
+ nil
72
+ end
73
+
74
+ def []= key, value
75
+ raise NotImplementedError, 'section writes not yet implemented'
76
+ end
77
+
78
+ def each
79
+ each_raw do |id, property_offset|
80
+ yield id, read_property(property_offset).last
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def each_raw
87
+ io.seek offset + 8
88
+ io.read(length * 8).scan(/.{8}/m).each { |str| yield(*str.unpack('V2')) }
89
+ end
90
+
91
+ def read_property property_offset
92
+ io.seek offset + property_offset
93
+ type, value = io.read(8).unpack('V2')
94
+ # is the method of serialization here custom?
95
+ case type
96
+ when VT_LPSTR, VT_LPWSTR
97
+ value = Variant.load type, io.read(value)
98
+ # ....
99
+ end
100
+ [type, value]
101
+ end
102
+ end
103
+
104
+ attr_reader :io, :signature, :unknown, :os, :guid, :sections
105
+
106
+ def initialize io
107
+ @io = io
108
+ load_header io.read(HEADER_SIZE)
109
+ load_section_list io.read(@num_sections * Section::SIZE)
110
+ # expect no gap between last section and start of data.
111
+ #Log.warn "gap between section list and property data" unless io.pos == @sections.map(&:offset).min
112
+ end
113
+
114
+ def load_header str
115
+ @signature, @unknown, @os_id, @guid, @num_sections = str.unpack HEADER_PACK
116
+ # should i check that unknown == 0? it usually is. so is the guid actually
117
+ @guid = Clsid.load @guid
118
+ @os = OS_MAP[@os_id] || Log.warn("unknown operating system id #{@os_id}")
119
+ end
120
+
121
+ def load_section_list str
122
+ @sections = str.scan(/.{#{Section::SIZE}}/m).map { |s| Section.new s, self }
123
+ end
124
+
125
+ def [] key
126
+ pair = PROPERTY_MAP[key.to_s] or return nil
127
+ section = @sections.find { |s| s.guid == pair.first } or return nil
128
+ section[pair.last]
129
+ end
130
+
131
+ def []= key, value
132
+ pair = PROPERTY_MAP[key.to_s] or return nil
133
+ section = @sections.find { |s| s.guid == pair.first } or return nil
134
+ section[pair.last] = value
135
+ end
136
+
137
+ def method_missing name, *args, &block
138
+ if name.to_s =~ /(.*)=$/
139
+ return super unless args.length == 1
140
+ return super unless PROPERTY_MAP[$1]
141
+ self[$1] = args.first
142
+ else
143
+ return super unless args.length == 0
144
+ return super unless PROPERTY_MAP[name.to_s]
145
+ self[name]
146
+ end
147
+ end
148
+
149
+ def each
150
+ @sections.each do |section|
151
+ next unless pair = DATA[section.guid]
152
+ map = pair.last
153
+ section.each do |id, value|
154
+ name = map[id] or next
155
+ yield name, value
156
+ end
157
+ end
158
+ end
159
+
160
+ def to_h
161
+ inject({}) { |hash, (name, value)| hash.update name.to_sym => value }
162
+ end
163
+ end
164
+ end
165
+ end
@@ -23,7 +23,7 @@
23
23
  TEST_DIR = File.dirname __FILE__
24
24
  $:.unshift "#{TEST_DIR}/../lib"
25
25
 
26
- require 'ole/file_system'
26
+ require 'ole/storage/file_system'
27
27
  require 'test/unit'
28
28
 
29
29
  module ExtraAssertions
@@ -146,7 +146,6 @@ class OleFsNonmutatingTest < Test::Unit::TestCase
146
146
  assert_equal(0, @ole.file.stat("dir2/dir21").size)
147
147
  end
148
148
 
149
- =begin
150
149
  def test_size?
151
150
  assert_equal(nil, @ole.file.size?("notAFile"))
152
151
  assert_equal(72, @ole.file.size?("file1"))
@@ -155,7 +154,6 @@ class OleFsNonmutatingTest < Test::Unit::TestCase
155
154
  assert_equal(72, @ole.file.stat("file1").size?)
156
155
  assert_equal(nil, @ole.file.stat("dir2/dir21").size?)
157
156
  end
158
- =end
159
157
 
160
158
  def test_file?
161
159
  assert(@ole.file.file?("file1"))
@@ -563,6 +561,14 @@ class OleFsFileStatTest < Test::Unit::TestCase
563
561
  assert_equal(64, @ole.file.stat("file1").blksize)
564
562
  end
565
563
 
564
+ # an additional test i added for coverage. i've tried to make the inspect
565
+ # string on the ole stat match that of the regular one.
566
+ def test_inspect
567
+ expect = '#<Ole::Storage::FileClass::Stat ino=0, uid=0, size=72, rdev=0, nlink=1, dev=0, blocks=2, gid=0, ftype=file, blksize=64>'
568
+ # normalize them, as instance_variables order is undefined
569
+ normalize = proc { |s| s[/ (.*)>$/, 1].split(', ').sort.join(', ') }
570
+ assert_equal normalize[expect], normalize[@ole.file.stat('file1').inspect]
571
+ end
566
572
  end
567
573
 
568
574
  class OleFsFileMutatingTest < Test::Unit::TestCase
@@ -659,7 +665,7 @@ class OleFsFileMutatingTest < Test::Unit::TestCase
659
665
 
660
666
  end
661
667
 
662
- class ZipFsDirectoryTest < Test::Unit::TestCase
668
+ class OleFsDirectoryTest < Test::Unit::TestCase
663
669
  def setup
664
670
  # we use an in memory copy of the file instead of the original
665
671
  # file based.
@@ -807,13 +813,12 @@ class ZipFsDirectoryTest < Test::Unit::TestCase
807
813
 
808
814
  end
809
815
 
810
- =begin
811
- class ZipFsDirIteratorTest < Test::Unit::TestCase
816
+ class OleFsDirIteratorTest < Test::Unit::TestCase
812
817
 
813
818
  FILENAME_ARRAY = [ "f1", "f2", "f3", "f4", "f5", "f6" ]
814
819
 
815
820
  def setup
816
- @dirIt = ZipFileSystem::ZipFsDirIterator.new(FILENAME_ARRAY)
821
+ @dirIt = Ole::Storage::DirClass::Dir.new('/', FILENAME_ARRAY)
817
822
  end
818
823
 
819
824
  def test_close
@@ -872,4 +877,3 @@ end
872
877
  # Copyright (C) 2002, 2003 Thomas Sondergaard
873
878
  # rubyzip is free software; you can redistribute it and/or
874
879
  # modify it under the terms of the ruby license.
875
- =end
@@ -3,8 +3,8 @@
3
3
  $: << File.dirname(__FILE__) + '/../lib'
4
4
 
5
5
  require 'test/unit'
6
- require 'ole/storage'
7
- require 'ole/file_system'
6
+ require 'ole/storage/base'
7
+ require 'ole/storage/file_system'
8
8
  require 'tempfile'
9
9
 
10
10
  class TestWriteMbat < Test::Unit::TestCase
@@ -0,0 +1,45 @@
1
+ #! /usr/bin/ruby
2
+
3
+ $: << File.dirname(__FILE__) + '/../lib'
4
+
5
+ require 'test/unit'
6
+ require 'ole/storage/base'
7
+ require 'ole/storage/meta_data'
8
+ require 'ole/storage/file_system'
9
+
10
+ class TestMetaData < Test::Unit::TestCase
11
+ def test_meta_data
12
+ Ole::Storage.open File.dirname(__FILE__) + '/test.doc', 'rb' do |ole|
13
+ assert_equal 'Charles Lowe', ole.meta_data[:doc_author]
14
+ assert_equal 'Charles Lowe', ole.meta_data['doc_author']
15
+ assert_equal 'Charles Lowe', ole.meta_data.to_h[:doc_author]
16
+ assert_equal 'Title', ole.meta_data.doc_title
17
+ assert_equal 'MSWordDoc', ole.meta_data.file_format
18
+ assert_equal 'application/msword', ole.meta_data.mime_type
19
+ assert_raises NotImplementedError do
20
+ ole.meta_data[:doc_author] = 'New Author'
21
+ end
22
+ end
23
+ end
24
+
25
+ # this tests the other ways of getting the mime_type, than using "\001CompObj",
26
+ # ie, relying on root clsid, and on the heuristics
27
+ def test_mime_type
28
+ ole = Ole::Storage.new StringIO.new
29
+ ole.root.clsid = Ole::Storage::MetaData::CLSID_EXCEL97.to_s
30
+ assert_equal nil, ole.meta_data.file_format
31
+ assert_equal 'application/vnd.ms-excel', ole.meta_data.mime_type
32
+
33
+ ole.root.clsid = 0.chr * Ole::Types::Clsid::SIZE
34
+ assert_equal nil, ole.meta_data.file_format
35
+ assert_equal nil, ole.meta_data.mime_type
36
+
37
+ ole.file.open('Book', 'w') { |f| }
38
+ assert_equal 'application/vnd.ms-excel', ole.meta_data.mime_type
39
+ ole.file.open('WordDocument', 'w') { |f| }
40
+ assert_equal 'application/msword', ole.meta_data.mime_type
41
+ ole.file.open('__properties_version1.0', 'w') { |f| }
42
+ assert_equal 'application/vnd.ms-outlook', ole.meta_data.mime_type
43
+ end
44
+ end
45
+
@@ -3,10 +3,9 @@
3
3
  $: << File.dirname(__FILE__) + '/../lib'
4
4
 
5
5
  require 'test/unit'
6
- require 'ole/storage'
7
- require 'ole/property_set'
6
+ require 'ole/types/property_set'
8
7
 
9
- class TestTypes < Test::Unit::TestCase
8
+ class TestPropertySet < Test::Unit::TestCase
10
9
  include Ole::Types
11
10
 
12
11
  def setup
@@ -25,16 +24,17 @@ class TestTypes < Test::Unit::TestCase
25
24
  assert_equal 14, section.length
26
25
  assert_equal 'f29f85e0-4ff9-1068-ab91-08002b27b3d9', section.guid.format
27
26
  assert_equal PropertySet::FMTID_SummaryInformation, section.guid
28
- assert_equal 'Charles Lowe', section.properties.assoc(4).last
29
- # new named support
30
- assert_equal 'Charles Lowe', section.doc_author
31
- end
32
-
33
- def test_ole_storage_integration
34
- Ole::Storage.open File.dirname(__FILE__) + '/test.doc', 'rb' do |ole|
35
- assert_equal 'Charles Lowe', ole.summary_info.doc_author
36
- assert_equal 'Title', ole.summary_info.doc_title
37
- end
27
+ assert_equal 'Charles Lowe', section.to_a.assoc(4).last
28
+ assert_equal 'Charles Lowe', propset.doc_author
29
+ assert_equal 'Charles Lowe', propset.to_h[:doc_author]
30
+
31
+ # knows the difference between existent and non-existent properties
32
+ assert_raise(NoMethodError) { propset.non_existent_key }
33
+ assert_raise(NotImplementedError) { propset.doc_author = 'New Author'}
34
+ assert_raise(NoMethodError) { propset.non_existent_key = 'Value'}
35
+
36
+ # a valid property that has no value in this property set
37
+ assert_equal nil, propset.security
38
38
  end
39
39
  end
40
40
 
@@ -96,5 +96,15 @@ class TestRangesIO < Test::Unit::TestCase
96
96
  # write enough to overflow the file
97
97
  assert_raises(IOError) { @io.write 'x' * 60 }
98
98
  end
99
+
100
+ def test_non_resizeable
101
+ # will try to truncate, which will fail
102
+ assert_raises NotImplementedError do
103
+ @io = RangesIO.new(StringIO.new, 'w', :ranges => [])
104
+ end
105
+ # will be fine
106
+ @io = RangesIONonResizeable.new(StringIO.new, 'w', :ranges => [])
107
+ assert_equal '#<IO::Mode wronly|creat>', @io.instance_variable_get(:@mode).inspect
108
+ end
99
109
  end
100
110
 
@@ -41,10 +41,6 @@ class TestStorageRead < Test::Unit::TestCase
41
41
  @ole.close
42
42
  end
43
43
 
44
- def test_invalid
45
- assert_raises(Ole::Storage::FormatError) { Ole::Storage.open StringIO.new(0.chr * 1024) }
46
- end
47
-
48
44
  def test_header
49
45
  # should have further header tests, testing the validation etc.
50
46
  assert_equal 17, @ole.header.to_a.length
@@ -53,6 +49,60 @@ class TestStorageRead < Test::Unit::TestCase
53
49
  assert_equal 1, @ole.header.num_sbat
54
50
  assert_equal 0, @ole.header.num_mbat
55
51
  end
52
+
53
+ def test_new_without_explicit_mode
54
+ open "#{TEST_DIR}/test_word_6.doc", 'rb' do |f|
55
+ assert_equal false, Ole::Storage.new(f).writeable
56
+ end
57
+ end
58
+
59
+ def capture_warnings
60
+ @warn = []
61
+ outer_warn = @warn
62
+ old_log = Ole::Log
63
+ begin
64
+ old_verbose = $VERBOSE
65
+ begin
66
+ $VERBOSE = nil
67
+ Ole.const_set :Log, Object.new
68
+ ensure
69
+ $VERBOSE = old_verbose
70
+ end
71
+ (class << Ole::Log; self; end).send :define_method, :warn do |message|
72
+ outer_warn << message
73
+ end
74
+ yield
75
+ ensure
76
+ old_verbose = $VERBOSE
77
+ begin
78
+ $VERBOSE = nil
79
+ Ole.const_set :Log, Object.new
80
+ ensure
81
+ $VERBOSE = old_verbose
82
+ end
83
+ end
84
+ end
85
+
86
+ def test_invalid
87
+ assert_raises Ole::Storage::FormatError do
88
+ Ole::Storage.open StringIO.new(0.chr * 1024)
89
+ end
90
+ assert_raises Ole::Storage::FormatError do
91
+ Ole::Storage.open StringIO.new(Ole::Storage::Header::MAGIC + 0.chr * 1024)
92
+ end
93
+ capture_warnings do
94
+ head = Ole::Storage::Header.new
95
+ head.threshold = 1024
96
+ assert_raises NoMethodError do
97
+ Ole::Storage.open StringIO.new(head.to_s + 0.chr * 1024)
98
+ end
99
+ end
100
+ assert_equal ['may not be a valid OLE2 structured storage file'], @warn
101
+ end
102
+
103
+ def test_inspect
104
+ assert_match(/#<Ole::Storage io=#<File:.*?test_word_6.doc> root=#<Dirent:"Root Entry">>/, @ole.inspect)
105
+ end
56
106
 
57
107
  def test_fat
58
108
  # the fat block has all the numbers from 5..118 bar 117
@@ -120,6 +170,36 @@ class TestStorageRead < Test::Unit::TestCase
120
170
  # i was actually not loading data correctly before, so carefully check everything here
121
171
  assert_equal expect, @ole.root.children.map { |child| child.read }
122
172
  end
173
+
174
+ def test_dirent
175
+ dirent = @ole.root.children.first
176
+ assert_equal '#<Dirent:"\001Ole" size=20 data="\001\000\000\002\000...">', dirent.inspect
177
+ assert_equal '#<Dirent:"Root Entry">', @ole.root.inspect
178
+
179
+ # exercise Dirent#[]. note that if you use a number, you get the Struct
180
+ # fields.
181
+ assert_equal dirent, @ole.root["\001Ole"]
182
+ assert_equal dirent.name_utf16, dirent[0]
183
+ assert_equal nil, @ole.root.time
184
+
185
+ assert_equal @ole.root.children, @ole.root.to_enum(:each_child).to_a
186
+
187
+ dirent.open('r') { |f| assert_equal 2, f.first_block }
188
+ dirent.open('w') { |f| }
189
+ assert_raises Errno::EINVAL do
190
+ dirent.open('a') { |f| }
191
+ end
192
+ end
193
+
194
+ def test_delete
195
+ dirent = @ole.root.children.first
196
+ assert_raises(ArgumentError) { @ole.root.delete nil }
197
+ assert_equal [dirent], @ole.root.children & [dirent]
198
+ assert_equal 20, dirent.size
199
+ @ole.root.delete dirent
200
+ assert_equal [], @ole.root.children & [dirent]
201
+ assert_equal 0, dirent.size
202
+ end
123
203
  end
124
204
 
125
205
  class TestStorageWrite < Test::Unit::TestCase
@@ -157,6 +237,13 @@ class TestStorageWrite < Test::Unit::TestCase
157
237
  Ole::Storage.open io, :update_timestamps => false, &:repack
158
238
  # note equivalence to the above flush, repack, flush
159
239
  assert_equal 'c8bb9ccacf0aaad33677e1b2a661ee6e66a48b5a', sha1(io.string)
240
+ # lets do it again using memory backing
241
+ Ole::Storage.open(io, :update_timestamps => false) { |ole| ole.repack :mem }
242
+ # note equivalence to the above flush, repack, flush
243
+ assert_equal 'c8bb9ccacf0aaad33677e1b2a661ee6e66a48b5a', sha1(io.string)
244
+ assert_raises ArgumentError do
245
+ Ole::Storage.open(io, :update_timestamps => false) { |ole| ole.repack :typo }
246
+ end
160
247
  end
161
248
 
162
249
  def test_create_from_scratch_hash