ruby-ole 1.2.2 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -4,8 +4,120 @@ require 'date'
4
4
  require 'ole/base'
5
5
 
6
6
  module Ole # :nodoc:
7
- # FIXME
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
- # the rest of this file is all a bit of adhoc marshal/unmarshal stuff
88
-
89
- # for VT_LPWSTR
90
- FROM_UTF16 = Iconv.new 'utf-8', 'utf-16le'
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
- # +time+ should be able to be either a Time, Date, or DateTime.
114
- def self.save_time time
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
- # for VT_CLSID
125
- # Convert a binary guid into a plain string (will move to proper class later).
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
@@ -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
+
@@ -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]
@@ -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
- assert_equal 'efa8cfaf833b30b1d1d9381771ddaafdfc95305c', sha1(io.string)
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)
@@ -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 \./test/test_support\.rb:\d+:test_logger\]\nWARN test$}
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