ruby-ole 1.2.6 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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