ruby-msg 1.2.17

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