ruby-ole 1.2.2 → 1.2.3
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 +22 -0
- data/Rakefile +9 -9
- data/lib/ole/file_system.rb +7 -7
- data/lib/ole/property_set.rb +31 -20
- data/lib/ole/ranges_io.rb +38 -44
- data/lib/ole/storage.rb +164 -110
- data/lib/ole/support.rb +80 -1
- data/lib/ole/types.rb +141 -40
- data/test/oleWithDirs.ole +0 -0
- data/test/test_SummaryInformation +0 -0
- data/test/test_mbat.rb +39 -0
- data/test/test_property_set.rb +30 -0
- data/test/test_ranges_io.rb +4 -4
- data/test/test_storage.rb +5 -1
- data/test/test_support.rb +39 -1
- data/test/test_types.rb +54 -0
- metadata +18 -6
data/lib/ole/support.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
#
|
3
2
|
# A file with general support functions used by most files in the project.
|
4
3
|
#
|
@@ -158,3 +157,83 @@ module RecursivelyEnumerable
|
|
158
157
|
protected :to_tree_helper
|
159
158
|
end
|
160
159
|
|
160
|
+
# can include File::Constants
|
161
|
+
class IO
|
162
|
+
BINARY = 0x4 unless defined?(BINARY)
|
163
|
+
|
164
|
+
# nabbed from rubinius, and modified
|
165
|
+
def self.parse_mode mode
|
166
|
+
ret = 0
|
167
|
+
|
168
|
+
case mode[0]
|
169
|
+
when ?r; ret |= RDONLY
|
170
|
+
when ?w; ret |= WRONLY | CREAT | TRUNC
|
171
|
+
when ?a; ret |= WRONLY | CREAT | APPEND
|
172
|
+
else raise ArgumentError, "illegal access mode #{mode}"
|
173
|
+
end
|
174
|
+
|
175
|
+
(1...mode.length).each do |i|
|
176
|
+
case mode[i]
|
177
|
+
when ?+; ret = (ret & ~(RDONLY | WRONLY)) | RDWR
|
178
|
+
when ?b; ret |= BINARY
|
179
|
+
else raise ArgumentError, "illegal access mode #{mode}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
ret
|
184
|
+
end
|
185
|
+
|
186
|
+
class Mode
|
187
|
+
NAMES = %w[rdonly wronly rdwr creat trunc append binary]
|
188
|
+
|
189
|
+
attr_reader :flags
|
190
|
+
def initialize flags
|
191
|
+
flags = IO.parse_mode flags.to_str if flags.respond_to? :to_str
|
192
|
+
raise ArgumentError, "invalid flags - #{flags.inspect}" unless Fixnum === flags
|
193
|
+
@flags = flags
|
194
|
+
end
|
195
|
+
|
196
|
+
def writeable?
|
197
|
+
#(@flags & IO::RDONLY) == 0
|
198
|
+
(@flags & 0x3) != IO::RDONLY
|
199
|
+
end
|
200
|
+
|
201
|
+
def readable?
|
202
|
+
(@flags & IO::WRONLY) == 0
|
203
|
+
end
|
204
|
+
|
205
|
+
def truncate?
|
206
|
+
(@flags & IO::TRUNC) != 0
|
207
|
+
end
|
208
|
+
|
209
|
+
def append?
|
210
|
+
(@flags & IO::APPEND) != 0
|
211
|
+
end
|
212
|
+
|
213
|
+
def create?
|
214
|
+
(@flags & IO::CREAT) != 0
|
215
|
+
end
|
216
|
+
|
217
|
+
def binary?
|
218
|
+
(@flags & IO::BINARY) != 0
|
219
|
+
end
|
220
|
+
|
221
|
+
=begin
|
222
|
+
# revisit this
|
223
|
+
def apply io
|
224
|
+
if truncate?
|
225
|
+
io.truncate 0
|
226
|
+
elsif append?
|
227
|
+
io.seek IO::SEEK_END, 0
|
228
|
+
end
|
229
|
+
end
|
230
|
+
=end
|
231
|
+
|
232
|
+
def inspect
|
233
|
+
names = NAMES.map { |name| name if (flags & IO.const_get(name.upcase)) != 0 }
|
234
|
+
names.unshift 'rdonly' if (flags & 0x3) == 0
|
235
|
+
"#<#{self.class} #{names.compact * '|'}>"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
data/lib/ole/types.rb
CHANGED
@@ -4,8 +4,120 @@ require 'date'
|
|
4
4
|
require 'ole/base'
|
5
5
|
|
6
6
|
module Ole # :nodoc:
|
7
|
-
#
|
7
|
+
#
|
8
|
+
# The Types module contains all the serialization and deserialization code for standard ole
|
9
|
+
# types.
|
10
|
+
#
|
11
|
+
# It also defines all the variant type constants, and symbolic names.
|
12
|
+
#
|
8
13
|
module Types
|
14
|
+
# for anything that we don't have serialization code for
|
15
|
+
class Data < String
|
16
|
+
def self.load str
|
17
|
+
new str
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.dump str
|
21
|
+
str.to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Lpstr < Data
|
26
|
+
end
|
27
|
+
|
28
|
+
# for VT_LPWSTR
|
29
|
+
class Lpwstr < String
|
30
|
+
FROM_UTF16 = Iconv.new 'utf-8', 'utf-16le'
|
31
|
+
TO_UTF16 = Iconv.new 'utf-16le', 'utf-8'
|
32
|
+
|
33
|
+
def self.load str
|
34
|
+
new FROM_UTF16.iconv(str)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.dump str
|
38
|
+
TO_UTF16.iconv str
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# for VT_FILETIME
|
43
|
+
class FileTime < DateTime
|
44
|
+
SIZE = 8
|
45
|
+
EPOCH = new 1601, 1, 1
|
46
|
+
|
47
|
+
# Create a +DateTime+ object from a struct +FILETIME+
|
48
|
+
# (http://msdn2.microsoft.com/en-us/library/ms724284.aspx).
|
49
|
+
#
|
50
|
+
# Converts +str+ to two 32 bit time values, comprising the high and low 32 bits of
|
51
|
+
# the 100's of nanoseconds since 1st january 1601 (Epoch).
|
52
|
+
def self.load str
|
53
|
+
low, high = str.to_s.unpack 'L2'
|
54
|
+
# we ignore these, without even warning about it
|
55
|
+
return nil if low == 0 and high == 0
|
56
|
+
# switched to rational, and fixed the off by 1 second error i sometimes got.
|
57
|
+
# time = EPOCH + (high * (1 << 32) + low) / 1e7 / 86400 rescue return
|
58
|
+
# use const_get to ensure we can return anything which subclasses this (VT_DATE?)
|
59
|
+
const_get('EPOCH') + Rational(high * (1 << 32) + low, 1e7.to_i * 86400) rescue return
|
60
|
+
# extra sanity check...
|
61
|
+
#unless (1800...2100) === time.year
|
62
|
+
# Log.warn "ignoring unlikely time value #{time.to_s}"
|
63
|
+
# return nil
|
64
|
+
#end
|
65
|
+
#time
|
66
|
+
end
|
67
|
+
|
68
|
+
# +time+ should be able to be either a Time, Date, or DateTime.
|
69
|
+
def self.dump time
|
70
|
+
# i think i'll convert whatever i get to be a datetime, because of
|
71
|
+
# the covered range.
|
72
|
+
return 0.chr * SIZE unless time
|
73
|
+
time = time.send(:to_datetime) if Time === time
|
74
|
+
# don't bother to use const_get here
|
75
|
+
bignum = (time - EPOCH) * 86400 * 1e7.to_i
|
76
|
+
high, low = bignum.divmod 1 << 32
|
77
|
+
[low, high].pack 'L2'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# for VT_CLSID
|
82
|
+
# Unlike most of the other conversions, the Guid's are serialized/deserialized by actually
|
83
|
+
# doing nothing! (eg, _load & _dump are null ops)
|
84
|
+
# Rather, its just a string with a different inspect string, and it includes a
|
85
|
+
# helper method for creating a Guid from that readable form (#format).
|
86
|
+
class Clsid < String
|
87
|
+
SIZE = 16
|
88
|
+
UNPACK = 'L S S CC C6'
|
89
|
+
|
90
|
+
def self.load str
|
91
|
+
new str.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.dump guid
|
95
|
+
return 0.chr * SIZE unless guid
|
96
|
+
# allow use of plain strings in place of guids.
|
97
|
+
guid['-'] ? parse(guid) : guid
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.parse str
|
101
|
+
vals = str.scan(/[a-f\d]+/i).map(&:hex)
|
102
|
+
if vals.length == 5
|
103
|
+
# this is pretty ugly
|
104
|
+
vals[3] = ('%04x' % vals[3]).scan(/../).map(&:hex)
|
105
|
+
vals[4] = ('%012x' % vals[4]).scan(/../).map(&:hex)
|
106
|
+
guid = new vals.flatten.pack(UNPACK)
|
107
|
+
return guid unless guid.delete('{}') == str.downcase.delete('{}')
|
108
|
+
end
|
109
|
+
raise ArgumentError, 'invalid guid - %p' % str
|
110
|
+
end
|
111
|
+
|
112
|
+
def format
|
113
|
+
"%08x-%04x-%04x-%02x%02x-#{'%02x' * 6}" % unpack(UNPACK)
|
114
|
+
end
|
115
|
+
|
116
|
+
def inspect
|
117
|
+
"#<#{self.class}:{#{format}}>"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
9
121
|
#
|
10
122
|
# The OLE variant types, extracted from
|
11
123
|
# http://www.marin.clara.net/COM/variant_type_definitions.htm.
|
@@ -77,55 +189,44 @@ module Ole # :nodoc:
|
|
77
189
|
0xffff => 'VT_ILLEGAL'
|
78
190
|
}
|
79
191
|
|
192
|
+
CLASS_MAP = {
|
193
|
+
# haven't seen one of these. wonder if its same as FILETIME?
|
194
|
+
#'VT_DATE' => ?,
|
195
|
+
'VT_LPSTR' => Lpstr,
|
196
|
+
'VT_LPWSTR' => Lpwstr,
|
197
|
+
'VT_FILETIME' => FileTime,
|
198
|
+
'VT_CLSID' => Clsid
|
199
|
+
}
|
200
|
+
|
80
201
|
module Constants
|
81
202
|
NAMES.each { |num, name| const_set name, num }
|
82
203
|
end
|
204
|
+
|
205
|
+
def self.load type, str
|
206
|
+
type = NAMES[type] or raise ArgumentError, 'unknown ole type - 0x%04x' % type
|
207
|
+
(CLASS_MAP[type] || Data).load str
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.dump type, variant
|
211
|
+
type = NAMES[type] or raise ArgumentError, 'unknown ole type - 0x%04x' % type
|
212
|
+
(CLASS_MAP[type] || Data).dump variant
|
213
|
+
end
|
83
214
|
end
|
84
215
|
|
85
216
|
include Variant::Constants
|
86
|
-
|
87
|
-
#
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
TO_UTF16 = Iconv.new 'utf-16le', 'utf-8'
|
92
|
-
|
93
|
-
# for VT_FILETIME
|
94
|
-
EPOCH = DateTime.parse '1601-01-01'
|
95
|
-
# Create a +DateTime+ object from a struct +FILETIME+
|
96
|
-
# (http://msdn2.microsoft.com/en-us/library/ms724284.aspx).
|
97
|
-
#
|
98
|
-
# Converts +str+ to two 32 bit time values, comprising the high and low 32 bits of
|
99
|
-
# the 100's of nanoseconds since 1st january 1601 (Epoch).
|
100
|
-
def self.load_time str
|
101
|
-
low, high = str.unpack 'L2'
|
102
|
-
# we ignore these, without even warning about it
|
103
|
-
return nil if low == 0 and high == 0
|
104
|
-
time = EPOCH + (high * (1 << 32) + low) / 1e7 / 86400 rescue return
|
105
|
-
# extra sanity check...
|
106
|
-
unless (1800...2100) === time.year
|
107
|
-
Log.warn "ignoring unlikely time value #{time.to_s}"
|
108
|
-
return nil
|
109
|
-
end
|
110
|
-
time
|
217
|
+
|
218
|
+
# deprecated aliases, kept mostly for the benefit of ruby-msg, until
|
219
|
+
# i release a new version.
|
220
|
+
def self.load_guid str
|
221
|
+
Variant.load VT_CLSID, str
|
111
222
|
end
|
112
223
|
|
113
|
-
|
114
|
-
|
115
|
-
# i think i'll convert whatever i get to be a datetime, because of
|
116
|
-
# the covered range.
|
117
|
-
return 0.chr * 8 unless time
|
118
|
-
time = time.send(:to_datetime) if Time === time
|
119
|
-
bignum = ((time - Ole::Types::EPOCH) * 86400 * 1e7.to_i)
|
120
|
-
high, low = bignum.divmod 1 << 32
|
121
|
-
[low, high].pack 'L2'
|
224
|
+
def self.load_time str
|
225
|
+
Variant.load VT_FILETIME, str
|
122
226
|
end
|
123
227
|
|
124
|
-
|
125
|
-
|
126
|
-
def self.load_guid str
|
127
|
-
"{%08x-%04x-%04x-%02x%02x-#{'%02x' * 6}}" % str.unpack('L S S CC C6')
|
128
|
-
end
|
228
|
+
FROM_UTF16 = Lpwstr::FROM_UTF16
|
229
|
+
TO_UTF16 = Lpwstr::TO_UTF16
|
129
230
|
end
|
130
231
|
end
|
131
232
|
|
Binary file
|
Binary file
|
data/test/test_mbat.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#! /usr/bin/ruby
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__) + '/../lib'
|
4
|
+
|
5
|
+
require 'test/unit'
|
6
|
+
require 'ole/storage'
|
7
|
+
require 'ole/file_system'
|
8
|
+
require 'tempfile'
|
9
|
+
|
10
|
+
class TestWriteMbat < Test::Unit::TestCase
|
11
|
+
def test_write_mbat
|
12
|
+
Tempfile.open 'myolefile' do |temp|
|
13
|
+
# this used to raise an error at flush time, due to failure to write the mbat
|
14
|
+
Ole::Storage.open temp do |ole|
|
15
|
+
# create a 10mb file
|
16
|
+
ole.file.open 'myfile', 'w' do |f|
|
17
|
+
s = 0.chr * 1_000_000
|
18
|
+
10.times { f.write s }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
assert((10_000_000..10_100_000) === temp.size, 'check file size')
|
23
|
+
|
24
|
+
Ole::Storage.open temp do |ole|
|
25
|
+
assert_equal 10_000_000, ole.file.size('myfile')
|
26
|
+
compare = ole.bbat.truncate[(0...ole.bbat.length).find { |i| ole.bbat[i] > 50_000 }..-1]
|
27
|
+
c = Ole::Storage::AllocationTable
|
28
|
+
# 10_000_000 * 4 / 512 / 512 rounded up is 153. but then there is room needed to store the
|
29
|
+
# bat in the bat, and the mbat too. hence 154.
|
30
|
+
expect = [c::EOC] * 2 + [c::BAT] * 154 + [c::META_BAT]
|
31
|
+
assert_equal expect, compare, 'allocation table structure'
|
32
|
+
# the sbat should be empty. in fact the file shouldn't exist at all, so the root's first
|
33
|
+
# block should be EOC
|
34
|
+
assert ole.sbat.empty?
|
35
|
+
assert_equal c::EOC, ole.root.first_block
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#! /usr/bin/ruby
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__) + '/../lib'
|
4
|
+
|
5
|
+
require 'test/unit'
|
6
|
+
require 'ole/property_set'
|
7
|
+
|
8
|
+
class TestTypes < Test::Unit::TestCase
|
9
|
+
include Ole::Types
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@io = open File.dirname(__FILE__) + '/test_SummaryInformation'
|
13
|
+
end
|
14
|
+
|
15
|
+
def teardown
|
16
|
+
@io.close
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_property_set
|
20
|
+
propset = PropertySet.new @io
|
21
|
+
assert_equal 1, propset.sections.length
|
22
|
+
section = propset.sections.first
|
23
|
+
assert_equal 14, section.length
|
24
|
+
assert_equal 'f29f85e0-4ff9-1068-ab91-08002b27b3d9', section.guid.format
|
25
|
+
assert_equal PropertySet::FMTID_SummaryInformation, section.guid
|
26
|
+
# i expect this null byte should have be stripped. need to fix the encoding functions.
|
27
|
+
assert_equal "Charles Lowe\000", section.properties.assoc(4).last
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
data/test/test_ranges_io.rb
CHANGED
@@ -12,7 +12,7 @@ class TestRangesIO < Test::Unit::TestCase
|
|
12
12
|
def setup
|
13
13
|
# read from ourself, also using overlaps.
|
14
14
|
ranges = [100..200, 0..10, 100..150]
|
15
|
-
@io = RangesIO.new open("#{TEST_DIR}/test_ranges_io.rb"), ranges, :close_parent => true
|
15
|
+
@io = RangesIO.new open("#{TEST_DIR}/test_ranges_io.rb"), :ranges => ranges, :close_parent => true
|
16
16
|
end
|
17
17
|
|
18
18
|
def teardown
|
@@ -23,9 +23,9 @@ class TestRangesIO < Test::Unit::TestCase
|
|
23
23
|
# block form
|
24
24
|
f = open("#{TEST_DIR}/test_ranges_io.rb")
|
25
25
|
assert_equal false, f.closed?
|
26
|
-
RangesIO.open f, []
|
26
|
+
RangesIO.open f, :ranges => []
|
27
27
|
assert_equal false, f.closed?
|
28
|
-
RangesIO.open(f, [], :close_parent => true) {}
|
28
|
+
RangesIO.open(f, :ranges => [], :close_parent => true) {}
|
29
29
|
assert_equal true, f.closed?
|
30
30
|
end
|
31
31
|
|
@@ -81,7 +81,7 @@ class TestRangesIO < Test::Unit::TestCase
|
|
81
81
|
|
82
82
|
def test_write
|
83
83
|
str = File.read "#{TEST_DIR}/test_ranges_io.rb"
|
84
|
-
@io = RangesIO.new StringIO.new(str), @io.ranges
|
84
|
+
@io = RangesIO.new StringIO.new(str), :ranges => @io.ranges
|
85
85
|
assert_equal "io'\nrequir", str[100, 10]
|
86
86
|
@io.write 'testing testing'
|
87
87
|
assert_equal 'testing te', str[100, 10]
|
data/test/test_storage.rb
CHANGED
@@ -6,6 +6,7 @@ require 'test/unit'
|
|
6
6
|
require 'ole/storage'
|
7
7
|
require 'digest/sha1'
|
8
8
|
require 'stringio'
|
9
|
+
require 'tempfile'
|
9
10
|
|
10
11
|
#
|
11
12
|
# = TODO
|
@@ -88,7 +89,10 @@ class TestStorageWrite < Test::Unit::TestCase
|
|
88
89
|
io = StringIO.open File.read("#{TEST_DIR}/test_word_6.doc")
|
89
90
|
assert_equal '9974e354def8471225f548f82b8d81c701221af7', sha1(io.string)
|
90
91
|
Ole::Storage.open(io, :update_timestamps => false) { }
|
91
|
-
|
92
|
+
# hash changed. used to be efa8cfaf833b30b1d1d9381771ddaafdfc95305c
|
93
|
+
# thats because i know truncate the io, and am probably removing some trailing allocated
|
94
|
+
# available blocks.
|
95
|
+
assert_equal 'a39e3c4041b8a893c753d50793af8d21ca8f0a86', sha1(io.string)
|
92
96
|
# add a repack test here
|
93
97
|
Ole::Storage.open io, :update_timestamps => false, &:repack
|
94
98
|
assert_equal 'c8bb9ccacf0aaad33677e1b2a661ee6e66a48b5a', sha1(io.string)
|
data/test/test_support.rb
CHANGED
@@ -25,7 +25,7 @@ class TestSupport < Test::Unit::TestCase
|
|
25
25
|
io = StringIO.new
|
26
26
|
log = Logger.new_with_callstack io
|
27
27
|
log.warn 'test'
|
28
|
-
expect = %r{^\[\d\d:\d\d:\d\d
|
28
|
+
expect = %r{^\[\d\d:\d\d:\d\d .*?test_support\.rb:\d+:test_logger\]\nWARN test$}
|
29
29
|
assert_match expect, io.string.chomp
|
30
30
|
end
|
31
31
|
|
@@ -46,6 +46,44 @@ class TestSupport < Test::Unit::TestCase
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
+
class TestIOMode < Test::Unit::TestCase
|
50
|
+
def mode s
|
51
|
+
IO::Mode.new s
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_parse
|
55
|
+
assert_equal true, mode('r+bbbbb').binary?
|
56
|
+
assert_equal false, mode('r+').binary?
|
57
|
+
|
58
|
+
assert_equal false, mode('r+').create?
|
59
|
+
assert_equal false, mode('r').create?
|
60
|
+
assert_equal true, mode('wb').create?
|
61
|
+
|
62
|
+
assert_equal true, mode('w').truncate?
|
63
|
+
assert_equal false, mode('r').truncate?
|
64
|
+
assert_equal false, mode('r+').truncate?
|
65
|
+
|
66
|
+
assert_equal true, mode('r+').readable?
|
67
|
+
assert_equal true, mode('r+').writeable?
|
68
|
+
assert_equal false, mode('r').writeable?
|
69
|
+
assert_equal false, mode('w').readable?
|
70
|
+
|
71
|
+
assert_equal true, mode('a').append?
|
72
|
+
assert_equal false, mode('w+').append?
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_invalid
|
76
|
+
assert_raises(ArgumentError) { mode 'rba' }
|
77
|
+
assert_raises(ArgumentError) { mode '+r' }
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_inspect
|
81
|
+
assert_equal '#<IO::Mode rdonly>', IO::Mode.new('r').inspect
|
82
|
+
assert_equal '#<IO::Mode rdwr|creat|trunc|binary>', IO::Mode.new('wb+').inspect
|
83
|
+
assert_equal '#<IO::Mode wronly|creat|append>', IO::Mode.new('a').inspect
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
49
87
|
class TestRecursivelyEnumerable < Test::Unit::TestCase
|
50
88
|
class Container
|
51
89
|
include RecursivelyEnumerable
|