ruby-msg 1.2.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,184 @@
1
+
2
+ # move to support?
3
+ class IO # :nodoc:
4
+ def self.copy src, dst
5
+ until src.eof?
6
+ buf = src.read(4096)
7
+ dst.write buf
8
+ end
9
+ end
10
+ end
11
+
12
+ #
13
+ # = Introduction
14
+ #
15
+ # +RangesIO+ is a basic class for wrapping another IO object allowing you to arbitrarily reorder
16
+ # slices of the input file by providing a list of ranges. Intended as an initial measure to curb
17
+ # inefficiencies in the Dirent#data method just reading all of a file's data in one hit, with
18
+ # no method to stream it.
19
+ #
20
+ # This class will encapuslate the ranges (corresponding to big or small blocks) of any ole file
21
+ # and thus allow reading/writing directly to the source bytes, in a streamed fashion (so just
22
+ # getting 16 bytes doesn't read the whole thing).
23
+ #
24
+ # In the simplest case it can be used with a single range to provide a limited io to a section of
25
+ # a file.
26
+ #
27
+ # = Limitations
28
+ #
29
+ # * No buffering. by design at the moment. Intended for large reads
30
+ #
31
+ # = TODO
32
+ #
33
+ # On further reflection, this class is something of a joining/optimization of
34
+ # two separate IO classes. a SubfileIO, for providing access to a range within
35
+ # a File as a separate IO object, and a ConcatIO, allowing the presentation of
36
+ # a bunch of io objects as a single unified whole.
37
+ #
38
+ # I will need such a ConcatIO if I'm to provide Mime#to_io, a method that will
39
+ # convert a whole mime message into an IO stream, that can be read from.
40
+ # It will just be the concatenation of a series of IO objects, corresponding to
41
+ # headers and boundaries, as StringIO's, and SubfileIO objects, coming from the
42
+ # original message proper, or RangesIO as provided by the Attachment#data, that
43
+ # will then get wrapped by Mime in a Base64IO or similar, to get encoded on-the-
44
+ # fly. Thus the attachment, in its plain or encoded form, and the message as a
45
+ # whole never exists as a single string in memory, as it does now. This is a
46
+ # fair bit of work to achieve, but generally useful I believe.
47
+ #
48
+ # This class isn't ole specific, maybe move it to my general ruby stream project.
49
+ #
50
+ class RangesIO
51
+ attr_reader :io, :ranges, :size, :pos
52
+ # +io+ is the parent io object that we are wrapping.
53
+ #
54
+ # +ranges+ are byte offsets, either
55
+ # 1. an array of ranges [1..2, 4..5, 6..8] or
56
+ # 2. an array of arrays, where the second is length [[1, 1], [4, 1], [6, 2]] for the above
57
+ # (think the way String indexing works)
58
+ # The +ranges+ provide sequential slices of the file that will be read. they can overlap.
59
+ def initialize io, ranges, opts={}
60
+ @opts = {:close_parent => false}.merge opts
61
+ @io = io
62
+ # convert ranges to arrays. check for negative ranges?
63
+ @ranges = ranges.map { |r| Range === r ? [r.begin, r.end - r.begin] : r }
64
+ # calculate size
65
+ @size = @ranges.inject(0) { |total, (pos, len)| total + len }
66
+ # initial position in the file
67
+ @pos = 0
68
+ end
69
+
70
+ def pos= pos, whence=IO::SEEK_SET
71
+ # FIXME support other whence values
72
+ raise NotImplementedError, "#{whence.inspect} not supported" unless whence == IO::SEEK_SET
73
+ # just a simple pos calculation. invalidate buffers if we had them
74
+ @pos = pos
75
+ end
76
+
77
+ alias seek :pos=
78
+ alias tell :pos
79
+
80
+ def close
81
+ @io.close if @opts[:close_parent]
82
+ end
83
+
84
+ def range_and_offset pos
85
+ off = nil
86
+ r = ranges.inject(0) do |total, r|
87
+ to = total + r[1]
88
+ if pos <= to
89
+ off = pos - total
90
+ break r
91
+ end
92
+ to
93
+ end
94
+ # should be impossible for any valid pos, (0...size) === pos
95
+ raise "unable to find range for pos #{pos.inspect}" unless off
96
+ [r, off]
97
+ end
98
+
99
+ def eof?
100
+ @pos == @size
101
+ end
102
+
103
+ # read bytes from file, to a maximum of +limit+, or all available if unspecified.
104
+ def read limit=nil
105
+ data = ''
106
+ limit ||= size
107
+ # special case eof
108
+ return data if eof?
109
+ r, off = range_and_offset @pos
110
+ i = ranges.index r
111
+ # this may be conceptually nice (create sub-range starting where we are), but
112
+ # for a large range array its pretty wasteful. even the previous way was. but
113
+ # i'm not trying to optimize this atm. it may even go to c later if necessary.
114
+ ([[r[0] + off, r[1] - off]] + ranges[i+1..-1]).each do |pos, len|
115
+ @io.seek pos
116
+ if limit < len
117
+ # FIXME this += isn't correct if there is a read error
118
+ # or something.
119
+ @pos += limit
120
+ break data << @io.read(limit)
121
+ end
122
+ # this can also stuff up. if the ranges are beyond the size of the file, we can get
123
+ # nil here.
124
+ data << @io.read(len)
125
+ @pos += len
126
+ limit -= len
127
+ end
128
+ data
129
+ end
130
+
131
+ # you may override this call to update @ranges and @size, if applicable. then write
132
+ # support can grow below
133
+ def truncate size
134
+ raise NotImplementedError, 'truncate not supported'
135
+ end
136
+ # why not? :)
137
+ alias size= :truncate
138
+
139
+ def write data
140
+ # short cut. needed because truncate 0 may return no ranges, instead of empty range,
141
+ # thus range_and_offset fails.
142
+ return 0 if data.empty?
143
+ data_pos = 0
144
+ # if we don't have room, we can use the truncate hook to make more space.
145
+ if data.length > @size - @pos
146
+ begin
147
+ truncate @pos + data.length
148
+ rescue NotImplementedError
149
+ # FIXME maybe warn instead, then just truncate the data?
150
+ raise "unable to satisfy write of #{data.length} bytes"
151
+ end
152
+ end
153
+ r, off = range_and_offset @pos
154
+ i = ranges.index r
155
+ ([[r[0] + off, r[1] - off]] + ranges[i+1..-1]).each do |pos, len|
156
+ @io.seek pos
157
+ if data_pos + len > data.length
158
+ chunk = data[data_pos..-1]
159
+ @io.write chunk
160
+ @pos += chunk.length
161
+ data_pos = data.length
162
+ break
163
+ end
164
+ @io.write data[data_pos, len]
165
+ @pos += len
166
+ data_pos += len
167
+ end
168
+ data_pos
169
+ end
170
+
171
+ # this will be generalised to a module later
172
+ def each_read blocksize=4096
173
+ yield read(blocksize) until eof?
174
+ end
175
+
176
+ def inspect
177
+ # the rescue is for empty files
178
+ pos, len = *(range_and_offset(@pos)[0] rescue [nil, nil])
179
+ range_str = pos ? "#{pos}..#{pos+len}" : 'nil'
180
+ "#<#{self.class} io=#{io.inspect} size=#@size pos=#@pos "\
181
+ "current_range=#{range_str}>"
182
+ end
183
+ end
184
+