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