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.
- 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
|