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.
- data/ChangeLog +12 -0
- data/README +27 -0
- data/Rakefile +2 -1
- data/bin/oletool +7 -1
- data/lib/ole/file_system.rb +2 -424
- data/lib/ole/storage.rb +3 -949
- data/lib/ole/storage/base.rb +948 -0
- data/lib/ole/storage/file_system.rb +444 -0
- data/lib/ole/storage/meta_data.rb +142 -0
- data/lib/ole/support.rb +23 -27
- data/lib/ole/types.rb +2 -243
- data/lib/ole/types/base.rb +247 -0
- data/lib/ole/types/property_set.rb +165 -0
- data/test/test_filesystem.rb +12 -8
- data/test/test_mbat.rb +2 -2
- data/test/test_meta_data.rb +45 -0
- data/test/test_property_set.rb +13 -13
- data/test/test_ranges_io.rb +10 -0
- data/test/test_storage.rb +91 -4
- data/test/test_types.rb +18 -5
- metadata +56 -42
- data/lib/ole/property_set.rb +0 -172
@@ -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
|
data/test/test_filesystem.rb
CHANGED
@@ -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
|
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
|
-
|
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 =
|
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
|
data/test/test_mbat.rb
CHANGED
@@ -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
|
+
|
data/test/test_property_set.rb
CHANGED
@@ -3,10 +3,9 @@
|
|
3
3
|
$: << File.dirname(__FILE__) + '/../lib'
|
4
4
|
|
5
5
|
require 'test/unit'
|
6
|
-
require 'ole/
|
7
|
-
require 'ole/property_set'
|
6
|
+
require 'ole/types/property_set'
|
8
7
|
|
9
|
-
class
|
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.
|
29
|
-
|
30
|
-
assert_equal 'Charles Lowe',
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
|
data/test/test_ranges_io.rb
CHANGED
@@ -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
|
|
data/test/test_storage.rb
CHANGED
@@ -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
|