rubytorrent 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ ## message.rb -- peer wire protocol message parsing/composition
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ ## we violate the users' namespaces here. but it's not in too
15
+ ## egregious of a way, and it's a royal pita to remove, so i'm keeping
16
+ ## it in for the time being.
17
+ class String
18
+ def from_fbbe # four-byte big-endian integer
19
+ raise "fbbe must be four-byte string (got #{self.inspect})" unless length == 4
20
+ (self[0] << 24) + (self[1] << 16) + (self[2] << 8) + self[3]
21
+ end
22
+ end
23
+
24
+ class Integer
25
+ def to_fbbe # four-byte big-endian integer
26
+ raise "fbbe must be < 2^32" unless self <= 2**32
27
+ raise "fbbe must be >= 0" unless self >= 0
28
+ s = " "
29
+ s[0] = (self >> 24) % 256
30
+ s[1] = (self >> 16) % 256
31
+ s[2] = (self >> 8) % 256
32
+ s[3] = (self ) % 256
33
+ s
34
+ end
35
+ end
36
+
37
+ module RubyTorrent
38
+
39
+ module StringExpandBits
40
+ include StringMapBytes
41
+
42
+ def expand_bits # just for debugging purposes
43
+ self.map_bytes do |b|
44
+ (0 .. 7).map { |i| ((b & (1 << (7 - i))) == 0 ? "0" : "1") }
45
+ end.flatten.join
46
+ end
47
+ end
48
+
49
+ class Message
50
+ WIRE_IDS = [:choke, :unchoke, :interested, :uninterested, :have, :bitfield,
51
+ :request, :piece, :cancel]
52
+
53
+ attr_accessor :id
54
+
55
+ def initialize(id, args=nil)
56
+ @id = id
57
+ @args = args
58
+ end
59
+
60
+ def method_missing(meth)
61
+ if @args.has_key? meth
62
+ @args[meth]
63
+ else
64
+ raise %{no such argument "#{meth}" to message #{self.to_s}}
65
+ end
66
+ end
67
+
68
+ def to_wire_form
69
+ case @id
70
+ when :keepalive
71
+ 0.to_fbbe
72
+ when :choke, :unchoke, :interested, :uninterested
73
+ 1.to_fbbe + WIRE_IDS.index(@id).chr
74
+ when :have
75
+ 5.to_fbbe + 4.chr + @args[:index].to_fbbe
76
+ when :bitfield
77
+ (@args[:bitfield].length + 1).to_fbbe + 5.chr + @args[:bitfield]
78
+ when :request, :cancel
79
+ 13.to_fbbe + WIRE_IDS.index(@id).chr + @args[:index].to_fbbe +
80
+ @args[:begin].to_fbbe + @args[:length].to_fbbe
81
+ when :piece
82
+ (@args[:length] + 9).to_fbbe + 7.chr + @args[:index].to_fbbe +
83
+ @args[:begin].to_fbbe
84
+ else
85
+ raise "unknown message type #{id}"
86
+ end
87
+ end
88
+
89
+ def self.from_wire_form(idnum, argstr)
90
+ type = WIRE_IDS[idnum]
91
+
92
+ case type
93
+ when :choke, :unchoke, :interested, :uninterested
94
+ raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.nil? or (argstr.length == 0)
95
+ Message.new(type)
96
+
97
+ when :have
98
+ raise ProtocolError, "invalid length #{str.length} for #{type} message" unless argstr.length == 4
99
+ Message.new(type, {:index => argstr[0,4].from_fbbe})
100
+
101
+ when :bitfield
102
+ Message.new(type, {:bitfield => argstr})
103
+
104
+ when :request, :cancel
105
+ raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 12
106
+ Message.new(type, {:index => argstr[0,4].from_fbbe,
107
+ :begin => argstr[4,4].from_fbbe,
108
+ :length => argstr[8,4].from_fbbe})
109
+ when :piece
110
+ raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 8
111
+ Message.new(type, {:index => argstr[0,4].from_fbbe,
112
+ :begin => argstr[4,4].from_fbbe})
113
+ else
114
+ raise "unknown message #{type.inspect}"
115
+ end
116
+ end
117
+
118
+ def to_s
119
+ case @id
120
+ when :bitfield
121
+ %{bitfield <#{@args[:bitfield].extend(StringExpandBits).expand_bits}>}
122
+ else
123
+ %{#@id#{@args.nil? ? "" : "(" + @args.map { |k, v| "#{k}=#{v.to_s.inspect}" }.join(", ") + ")"}}
124
+ end
125
+ end
126
+ end
127
+
128
+ end
@@ -0,0 +1,214 @@
1
+ ## metainfo.rb -- parsed .torrent file
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require "rubytorrent/typedstruct"
15
+ require 'uri'
16
+ require 'open-uri'
17
+ require 'digest/sha1'
18
+
19
+ ## MetaInfo file is the parsed form of the .torrent file that people
20
+ ## send around. It contains a MetaInfoInfo and possibly some
21
+ ## MetaInfoInfoFile objects.
22
+ module RubyTorrent
23
+
24
+ class MetaInfoFormatError < StandardError; end
25
+
26
+ class MetaInfoInfoFile
27
+ def initialize(dict=nil)
28
+ @s = TypedStruct.new do |s|
29
+ s.field :length => Integer, :md5sum => String, :sha1 => String,
30
+ :path => String
31
+ s.array :path
32
+ s.required :length, :path
33
+ end
34
+
35
+ @dict = dict
36
+ unless dict.nil?
37
+ @s.parse dict
38
+ check
39
+ end
40
+ end
41
+
42
+ def method_missing(meth, *args)
43
+ @s.send(meth, *args)
44
+ end
45
+
46
+ def check
47
+ raise MetaInfoFormatError, "invalid file length" unless @s.length >= 0
48
+ end
49
+
50
+ def to_bencoding
51
+ check
52
+ (@dict || @s).to_bencoding
53
+ end
54
+ end
55
+
56
+ class MetaInfoInfo
57
+ def initialize(dict=nil)
58
+ @s = TypedStruct.new do |s|
59
+ s.field :length => Integer, :md5sum => String, :name => String,
60
+ :piece_length => Integer, :pieces => String,
61
+ :files => MetaInfoInfoFile, :sha1 => String
62
+ s.label :piece_length => "piece length"
63
+ s.required :name, :piece_length, :pieces
64
+ s.array :files
65
+ s.coerce :files => lambda { |x| x.map { |y| MetaInfoInfoFile.new(y) } }
66
+ end
67
+
68
+ @dict = dict
69
+ unless dict.nil?
70
+ @s.parse dict
71
+ check
72
+ if dict["sha1"]
73
+ ## this seems to always be off. don't know how it's supposed
74
+ ## to be calculated, so fuck it.
75
+ # puts "we have #{sha1.inspect}, they have #{dict['sha1'].inspect}"
76
+ # rt_warning "info hash SHA1 mismatch" unless dict["sha1"] == sha1
77
+ # raise MetaInfoFormatError, "info hash SHA1 mismatch" unless dict["sha1"] == sha1
78
+ end
79
+ end
80
+ end
81
+
82
+ def check
83
+ raise MetaInfoFormatError, "invalid file length" unless @s.length.nil? || @s.length >= 0
84
+ raise MetaInfoFormatError, "one (and only one) of 'length' (single-file torrent) or 'files' (multi-file torrent) must be specified" if (@s.length.nil? && @s.files.nil?) || (!@s.length.nil? && !@s.files.nil?)
85
+ if single?
86
+ length = @s.length
87
+ else
88
+ length = @s.files.inject(0) { |s, x| s + x.length }
89
+ end
90
+ raise MetaInfoFormatError, "invalid metainfo file: length #{length} > (#{@s.pieces.length / 20} pieces * #{@s.piece_length})" unless length <= (@s.pieces.length / 20) * @s.piece_length
91
+ raise MetaInfoFormatError, "invalid metainfo file: pieces length = #{@s.pieces.length} not a multiple of 20" unless (@s.pieces.length % 20) == 0
92
+ end
93
+
94
+ def to_bencoding
95
+ check
96
+ (@dict || @s).to_bencoding
97
+ end
98
+
99
+ def sha1
100
+ if @s.dirty
101
+ @sha1 = Digest::SHA1.digest(self.to_bencoding)
102
+ @s.dirty = false
103
+ end
104
+ @sha1
105
+ end
106
+
107
+ def single?
108
+ !length.nil?
109
+ end
110
+
111
+ def multiple?
112
+ length.nil?
113
+ end
114
+
115
+ def total_length
116
+ if single?
117
+ length
118
+ else
119
+ files.inject(0) { |a, f| a + f.length }
120
+ end
121
+ end
122
+
123
+ def num_pieces
124
+ pieces.length / 20
125
+ end
126
+
127
+ def method_missing(meth, *args)
128
+ @s.send(meth, *args)
129
+ end
130
+ end
131
+
132
+ class MetaInfo
133
+ def initialize(dict=nil)
134
+ raise TypeError, "argument must be a Hash (maybe see MetaInfo.from_location)" unless dict.is_a? Hash
135
+ @s = TypedStruct.new do |s|
136
+ s.field :info => MetaInfoInfo, :announce => URI::HTTP,
137
+ :announce_list => Array, :creation_date => Time,
138
+ :comment => String, :created_by => String, :encoding => String
139
+ s.label :announce_list => "announce-list", :creation_date => "creation date",
140
+ :created_by => "created by"
141
+ s.array :announce_list
142
+ s.coerce :info => lambda { |x| MetaInfoInfo.new(x) },
143
+ :creation_date => lambda { |x| Time.at(x) },
144
+ :announce => lambda { |x| URI.parse(x) },
145
+ :announce_list => lambda { |x| x.map { |y| y.map { |z| URI.parse(z) } } }
146
+ end
147
+
148
+ @dict = dict
149
+ unless dict.nil?
150
+ @s.parse dict
151
+ check
152
+ end
153
+ end
154
+
155
+ def single?; info.single?; end
156
+ def multiple?; info.multiple?; end
157
+
158
+ def check
159
+ if @s.announce_list
160
+ @s.announce_list.each do |tier|
161
+ tier.each { |track| raise MetaInfoFormatError, "expecting HTTP URL in announce-list, got #{track} instead" unless track.is_a? URI::HTTP }
162
+ end
163
+ end
164
+ end
165
+
166
+ def self.from_bstream(bs)
167
+ dict = nil
168
+ bs.each do |e|
169
+ if dict == nil
170
+ dict = e
171
+ else
172
+ raise MetaInfoFormatError, "too many bencoded elements for metainfo file (just need one)"
173
+ end
174
+ end
175
+
176
+ raise MetaInfoFormatError, "bencoded element must be a dictionary, got a #{dict.class}" unless dict.kind_of? ::Hash
177
+
178
+ MetaInfo.new(dict)
179
+ end
180
+
181
+ ## either a filename or a URL
182
+ def self.from_location(fn, http_proxy=ENV["http_proxy"])
183
+ if http_proxy # lame!
184
+ open(fn, "rb", :proxy => http_proxy) { |f| from_bstream(BStream.new(f)) }
185
+ else
186
+ open(fn, "rb") { |f| from_bstream(BStream.new(f)) }
187
+ end
188
+ end
189
+
190
+ def self.from_stream(s)
191
+ from_bstream(BStream.new(s))
192
+ end
193
+
194
+ def method_missing(meth, *args)
195
+ @s.send(meth, *args)
196
+ end
197
+
198
+ def to_bencoding
199
+ check
200
+ (@dict || @s).to_bencoding
201
+ end
202
+
203
+ def trackers
204
+ if announce_list && (announce_list.length > 0)
205
+ announce_list.map do |tier|
206
+ tier.extend(ArrayShuffle).shuffle
207
+ end.flatten
208
+ else
209
+ [announce]
210
+ end
211
+ end
212
+ end
213
+
214
+ end
@@ -0,0 +1,595 @@
1
+ ## package.rb -- RubyTorrent <=> filesystem interface.
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'thread'
15
+ require 'digest/sha1'
16
+
17
+ ## A Package is the connection between the network and the
18
+ ## filesystem. There is one Package per torrent. Each Package is
19
+ ## composed of one or more Pieces, as determined by the MetaInfoInfo
20
+ ## object, and each Piece is composed of one or more Blocks, which are
21
+ ## transmitted over the PeerConnection with :piece comments.
22
+
23
+ module RubyTorrent
24
+
25
+ ## Range plus a lot of utility methods
26
+ class AwesomeRange < Range
27
+ def initialize(start, endd=nil, exclude_end=false)
28
+ case start
29
+ when Integer
30
+ raise ArgumentError, "both start and endd must be specified" if endd.nil?
31
+ super(start, endd, exclude_end)
32
+ when Range
33
+ super(start.first, start.last, start.exclude_end?)
34
+ else
35
+ raise ArgumentError, "start should be an Integer or a Range, is a #{start.class}"
36
+ end
37
+ end
38
+
39
+ ## range super-set: does this range encompass 'o'?
40
+ def rss?(o)
41
+ (first <= o.first) &&
42
+ ((last > o.last) || (o.exclude_end? && (last == o.last)))
43
+ end
44
+
45
+ ## range intersection
46
+ def rint(o)
47
+ ## three cases. either:
48
+ ## a) our left endpoint is within o
49
+ if ((first >= o.first) &&
50
+ ((first < o.last) || (!o.exclude_end? && (first == o.last))))
51
+ if last < o.last
52
+ AwesomeRange.new(first, last, exclude_end?)
53
+ elsif last > o.last
54
+ AwesomeRange.new(first, o.last, o.exclude_end?)
55
+ else # ==
56
+ AwesomeRange.new(first, last, exclude_end? || o.exclude_end?)
57
+ end
58
+ ## b) our right endpoint is within o
59
+ elsif (((last > o.first) || (!exclude_end? && (last == o.first))) && ((last < o.last) || (!o.exclude_end? && (last == o.last))))
60
+ AwesomeRange.new([first, o.first].max, last, exclude_end?)
61
+ ## c) we encompass o
62
+ elsif rss?(o)
63
+ o
64
+ else
65
+ nil
66
+ end
67
+ end
68
+
69
+ ## range continuity
70
+ def rcont?(o)
71
+ (first == o.last) || (last == o.first) || (rint(o) != nil)
72
+ end
73
+
74
+ ## range union: only valid for continuous ranges
75
+ def runion(o)
76
+ if last > o.last
77
+ AwesomeRange.new([first, o.first].min, last, exclude_end?)
78
+ elsif o.last > last
79
+ AwesomeRange.new([first, o.first].min, o.last, o.exclude_end?)
80
+ else # equal
81
+ AwesomeRange.new([first, o.first].min, last, (exclude_end? && o.exclude_end?))
82
+ end
83
+ end
84
+
85
+ ## range difference. returns an array of 0, 1 or 2 ranges.
86
+ def rdiff(o)
87
+ return [] if o == self
88
+ ret = []
89
+ int = rint o
90
+ return [] if int == self
91
+ return [self] if int == nil
92
+ raise RangeError, "can't subtract a range that doesn't have an exclusive end" unless int.exclude_end?
93
+ if int.first > first
94
+ ret << AwesomeRange.new(first, int.first, true)
95
+ end
96
+ ret + [AwesomeRange.new(int.last, last, exclude_end?)]
97
+ end
98
+ end
99
+
100
+ ## a Covering is a set of non-overlapping ranges within a given start
101
+ ## point and endpoint.
102
+ class Covering
103
+ attr_accessor :domain, :ranges
104
+
105
+ ## 'domain' should be an AwesomeRange determining the start and end
106
+ ## point. 'ranges' should be an array of non-overlapping
107
+ ## AwesomeRanges sorted by start point.
108
+ def initialize(domain, ranges=[])
109
+ @domain = domain
110
+ @ranges = ranges
111
+ end
112
+
113
+ def complete!; @ranges = [@domain]; self; end
114
+ def complete?; @ranges == [@domain]; end
115
+ def empty!; @ranges = []; self; end
116
+ def empty?; @ranges == []; end
117
+
118
+ ## given a covering of size N and a new range 'r', returns a
119
+ ## covering of size 0 <= s <= N + 1 that doesn't cover the range
120
+ ## given by 'r'.
121
+ def poke(r)
122
+ raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
123
+ Covering.new(@domain, @ranges.inject([]) do |set, x|
124
+ if x.rint(r) != nil
125
+ set + x.rdiff(r)
126
+ else
127
+ set + [x]
128
+ end
129
+ end)
130
+ end
131
+
132
+ ## given a covering of size N and a new range 'r', returns a
133
+ ## covering of size 0 < s <= N + 1 that also covers the range 'r'.
134
+ def fill(r)
135
+ raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
136
+ Covering.new(@domain, @ranges.inject([]) do |set, x|
137
+ ## r contains the result of the continuing merge. if r is nil,
138
+ ## then we've already added it, so we just copy x.
139
+ if r.nil? then set + [x] else
140
+ ## otoh, if r is there, we try and merge in the current
141
+ ## element.
142
+ if r.rcont? x
143
+ ## if we can merge, keep the union in r and don't add
144
+ ## anything
145
+ r = r.runion x
146
+ set
147
+ ## if we can't merge it, we'll see if it's time to add it. we
148
+ ## know that r and x don't overlap because r.mergable?(x) was
149
+ ## false, so we can simply compare the start points to see
150
+ ## whether it should come before x.
151
+ elsif r.first < x.first
152
+ s = set + [r, x] # add both
153
+ r = nil
154
+ s
155
+ else set + [x] ## no merging or adding, so we just copy x.
156
+ end
157
+ end
158
+ ## if 'r' still hasn't been added, it should be the last element,
159
+ ## we add it here.
160
+ end.push(r).compact)
161
+ end
162
+
163
+ ## given an array of non-overlapping ranges sorted by start point,
164
+ ## and a range 'domain', returns the first range from 'domain' not
165
+ ## covered by any range in the array.
166
+ def first_gap(domain=@domain)
167
+ start = domain.first
168
+ endd = nil
169
+ excl = nil
170
+ @ranges.each do |r|
171
+ next if r.last < start
172
+
173
+ if r.first > start # found a gap
174
+ if r.first < domain.last
175
+ return AwesomeRange.new(start, r.first, false)
176
+ else # r.first >= domain.last, so use domain's exclusion
177
+ return AwesomeRange.new(start, domain.last, domain.exclude_end?)
178
+ end
179
+ else # r.first <= start
180
+ start = r.last unless r.last < start
181
+ break if start > domain.last
182
+ end
183
+ end
184
+
185
+ if (start >= domain.last)
186
+ ## entire domain was covered
187
+ nil
188
+ else
189
+ ## tail end of the domain uncovered
190
+ AwesomeRange.new(start, domain.last, domain.exclude_end?)
191
+ end
192
+ end
193
+
194
+ def ==(o); o.domain == self.domain && o.ranges == self.ranges; end
195
+ end
196
+
197
+ ## Blocks are very simple chunks of data which exist solely in
198
+ ## memory. they are the basic currency of the bittorrent protocol. a
199
+ ## Block can be divided into "chunks" (no intelligence there; it's
200
+ ## solely for the purposes of buffered reading/writing) and one or
201
+ ## more Blocks comprises a Piece.
202
+ class Block
203
+ attr_accessor :pindex, :begin, :length, :data, :requested
204
+
205
+ def initialize(pindex, beginn, length)
206
+ @pindex = pindex
207
+ @begin = beginn
208
+ @length = length
209
+ @data = nil
210
+ @requested = false
211
+ @time = nil
212
+ end
213
+
214
+ def requested?; @requested; end
215
+ def have_length; @data.length; end
216
+ def complete?; @data && (@data.length == @length); end
217
+ def mark_time; @time = Time.now; end
218
+ def time_elapsed; Time.now - @time; end
219
+
220
+ def to_s
221
+ "<block: p[#{@pindex}], #@begin + #@length #{(data.nil? || (data.length == 0) ? 'emp' : (complete? ? 'cmp' : 'inc'))}>"
222
+ end
223
+
224
+ ## chunk can only be added to blocks in order
225
+ def add_chunk(chunk)
226
+ @data = "" if @data.nil?
227
+ raise "adding chunk would result in too much data (#{@data.length} + #{chunk.length} > #@length)" if (@data.length + chunk.length) > @length
228
+ @data += chunk
229
+ self
230
+ end
231
+
232
+ def each_chunk(blocksize)
233
+ raise "each_chunk called on incomplete block" unless complete?
234
+ start = 0
235
+ while(start < @length)
236
+ yield data[start, [blocksize, @length - start].min]
237
+ start += blocksize
238
+ end
239
+ end
240
+
241
+ def ==(o)
242
+ o.is_a?(Block) && (o.pindex == self.pindex) && (o.begin == self.begin) &&
243
+ (o.length == self.length)
244
+ end
245
+ end
246
+
247
+ ## a Piece is the basic unit of the .torrent metainfo file (though not
248
+ ## of the bittorrent protocol). Pieces store their data directly on
249
+ ## disk, so many operations here will be slow. each Piece stores data
250
+ ## in one or more file pointers.
251
+ ##
252
+ ## unlike Blocks and Packages, which are either complete or
253
+ ## incomplete, a Piece can be complete but not valid, if the SHA1
254
+ ## check fails. thus, a call to piece.complete? is not sufficient to
255
+ ## determine whether the data is ok to use or not.
256
+ ##
257
+ ## Pieces handle all the trickiness involved with Blocks: taking in
258
+ ## Blocks from arbitrary locations, writing them out to the correct
259
+ ## set of file pointers, keeping track of which sections of the data
260
+ ## have been filled, claimed but not filled, etc.
261
+ class Piece
262
+ include EventSource
263
+
264
+ attr_reader :index, :start, :length
265
+ event :complete
266
+
267
+ def initialize(index, sha1, start, length, files, validity_assumption=nil)
268
+ @index = index
269
+ @sha1 = sha1
270
+ @start = start
271
+ @length = length
272
+ @files = files # array of [file pointer, mutex, file length]
273
+ @valid = nil
274
+
275
+ ## calculate where we start and end in terms of the file pointers.
276
+ @start_index = 0
277
+ sum = 0
278
+ while(sum + @files[@start_index][2] <= @start)
279
+ sum += @files[@start_index][2]
280
+ @start_index += 1
281
+ end
282
+ ## now sum + @files[@start_index][2] > start, and sum <= start
283
+ @start_offset = @start - sum
284
+
285
+ ## sections of the data we have
286
+ @have = Covering.new(AwesomeRange.new(0 ... @length)).complete!
287
+ @valid = validity_assumption
288
+ @have.empty! unless valid?
289
+
290
+ ## sections of the data someone has laid claim to but hasn't yet
291
+ ## provided. a super-set of @have.
292
+ @claimed = Covering.new(AwesomeRange.new(0 ... @length))
293
+
294
+ ## protects @claimed, @have
295
+ @state_m = Mutex.new
296
+ end
297
+
298
+ def to_s
299
+ "<piece #@index: #@start + #@length #{(complete? ? 'cmp' : 'inc')}>"
300
+ end
301
+
302
+ def complete?; @have.complete?; end
303
+ def started?; !@claimed.empty? || !@have.empty?; end
304
+
305
+ def discard # discard all data
306
+ @state_m.synchronize do
307
+ @have.empty!
308
+ @claimed.empty!
309
+ end
310
+ @valid = false
311
+ end
312
+
313
+ def valid?
314
+ return @valid unless @valid.nil?
315
+ return (@valid = false) unless complete?
316
+
317
+ data = read_bytes(0, @length)
318
+ if (data.length != @length)
319
+ @valid = false
320
+ else
321
+ @valid = (Digest::SHA1.digest(data) == @sha1)
322
+ end
323
+ end
324
+
325
+ def unclaimed_bytes
326
+ r = 0
327
+ each_gap(@claimed) { |start, len| r += len }
328
+ r
329
+ end
330
+
331
+ def empty_bytes
332
+ r = 0
333
+ each_gap(@have) { |start, len| r += len }
334
+ r
335
+ end
336
+
337
+ def percent_claimed; 100.0 * (@length.to_f - unclaimed_bytes) / @length; end
338
+ def percent_done; 100.0 * (@length.to_f - empty_bytes) / @length; end
339
+
340
+ def each_unclaimed_block(max_length)
341
+ raise "no unclaimed blocks in a complete piece" if complete?
342
+
343
+ each_gap(@claimed, max_length) do |start, len|
344
+ yield Block.new(@index, start, len)
345
+ end
346
+ end
347
+
348
+ def each_empty_block(max_length)
349
+ raise "no empty blocks in a complete piece" if complete?
350
+
351
+ each_gap(@have, max_length) do |start, len|
352
+ yield Block.new(@index, start, len)
353
+ end
354
+ end
355
+
356
+ def claim_block(b)
357
+ @state_m.synchronize do
358
+ @claimed = @claimed.fill AwesomeRange.new(b.begin ... (b.begin + b.length))
359
+ end
360
+ end
361
+
362
+ def unclaim_block(b)
363
+ @state_m.synchronize do
364
+ @claimed = @claimed.poke AwesomeRange.new(b.begin ... (b.begin + b.length))
365
+ end
366
+ end
367
+
368
+ ## for a complete Piece, returns a complete Block of specified size
369
+ ## and location.
370
+ def get_complete_block(beginn, length)
371
+ raise "can't make block from incomplete piece" unless complete?
372
+ raise "invalid parameters #{beginn}, #{length}" unless (length > 0) && (beginn + length) <= @length
373
+
374
+ b = Block.new(@index, beginn, length)
375
+ b.add_chunk read_bytes(beginn, length) # returns b
376
+ end
377
+
378
+ ## we don't do any checking that this block has been claimed or not.
379
+ def add_block(b)
380
+ @valid = nil
381
+ write = false
382
+ new_have = @state_m.synchronize { @have.fill AwesomeRange.new(b.begin ... (b.begin + b.length)) }
383
+ if new_have != @have
384
+ @have = new_have
385
+ write = true
386
+ end
387
+
388
+ write_bytes(b.begin, b.data) if write
389
+ send_event(:complete) if complete?
390
+ end
391
+
392
+ private
393
+
394
+ ## yields successive gaps from 'array' between 0 and @length
395
+ def each_gap(covering, max_length=nil)
396
+ return if covering.complete?
397
+
398
+ range_first = 0
399
+ while true
400
+ range = covering.first_gap(range_first ... @length)
401
+ break if range.nil? || (range.first == range.last)
402
+ start = range.first
403
+
404
+ while start < range.last
405
+ len = range.last - start
406
+ len = max_length if max_length && (max_length < len)
407
+ yield start, len
408
+ start += len
409
+ end
410
+
411
+ range_first = range.last
412
+ end
413
+ end
414
+
415
+ def write_bytes(start, data); do_bytes(start, 0, data); end
416
+ def read_bytes(start, length); do_bytes(start, length, nil); end
417
+
418
+ ## do the dirty work of splitting the read/writes across multiple
419
+ ## file pointers to possibly incomplete, possibly overcomplete files
420
+ def do_bytes(start, length, data)
421
+ raise ArgumentError, "invalid start" if (start < 0) || (start > @length)
422
+ # raise "invalid length" if (length < 0) || (start + length > @length)
423
+
424
+ start += @start_offset
425
+ index = @start_index
426
+ sum = 0
427
+ while(sum + @files[index][2] <= start)
428
+ sum += @files[index][2]
429
+ index += 1
430
+ end
431
+ offset = start - sum
432
+
433
+ done = 0
434
+ abort = false
435
+ if data.nil?
436
+ want = length
437
+ ret = ""
438
+ else
439
+ want = data.length
440
+ ret = 0
441
+ end
442
+ while (done < want) && !abort
443
+ break if index > @files.length
444
+ fp, mutex, size = @files[index]
445
+ mutex.synchronize do
446
+ fp.seek offset
447
+ here = [want - done, size - offset].min
448
+ if data.nil?
449
+ # puts "> reading #{here} bytes from #{index} at #{offset}"
450
+ s = fp.read here
451
+ # puts "> got #{(s.nil? ? s.inspect : s.length)} bytes"
452
+ if s.nil?
453
+ abort = true
454
+ else
455
+ ret += s
456
+ abort = true if s.length < here
457
+ # puts "fp.tell is #{fp.tell}, size is #{size}, eof #{fp.eof?}"
458
+ if (fp.tell == size) && !fp.eof?
459
+ rt_warning "file #{index}: not at eof after #{size} bytes, truncating"
460
+ fp.truncate(size - 1)
461
+ end
462
+ end
463
+ else
464
+ # puts "> writing #{here} bytes to #{index} at #{offset}"
465
+ x = fp.write data[done, here]
466
+ ret += here
467
+ # @files[index][0].flush
468
+ end
469
+ done += here
470
+ end
471
+ index += 1
472
+ offset = 0
473
+ end
474
+
475
+ ret
476
+ end
477
+ end
478
+
479
+ ## finally, the Package. one Package per Controller so we don't do any
480
+ ## thread safety stuff in here.
481
+ class Package
482
+ include EventSource
483
+
484
+ attr_reader :pieces, :size
485
+ event :complete
486
+
487
+ def initialize(metainfo, out=nil, validity_assumption=nil)
488
+ info = metainfo.info
489
+
490
+ created = false
491
+ out ||= info.name
492
+ case out
493
+ when File
494
+ raise ArgumentError, "'out' cannot be a File for a multi-file .torrent" if info.multiple?
495
+ fstream = out
496
+ when Dir
497
+ raise ArgumentError, "'out' cannot be a Dir for a single-file .torrent" if info.single?
498
+ fstream = out
499
+ when String
500
+ if info.single?
501
+ rt_debug "output file is #{out}"
502
+ begin
503
+ fstream = File.open(out, "rb+")
504
+ rescue Errno::ENOENT
505
+ created = true
506
+ fstream = File.open(out, "wb+")
507
+ end
508
+ else
509
+ rt_debug "output directory is #{out}"
510
+ unless File.exists? out
511
+ Dir.mkdir(out)
512
+ created = true
513
+ end
514
+ fstream = Dir.open(out)
515
+ end
516
+ else
517
+ raise ArgumentError, "'out' should be a File, Dir or String object, is #{out.class}"
518
+ end
519
+
520
+ @ro = false
521
+ @size = info.total_length
522
+ if info.single?
523
+ @files = [[fstream, Mutex.new, info.length]]
524
+ else
525
+ @files = info.files.map do |finfo|
526
+ path = File.join(finfo.path[0, finfo.path.length - 1].inject(fstream.path) do |path, el|
527
+ dir = File.join(path, el)
528
+ unless File.exist? dir
529
+ rt_debug "making directory #{dir}"
530
+ Dir.mkdir dir
531
+ end
532
+ dir
533
+ end, finfo.path[finfo.path.length - 1])
534
+ rt_debug "opening #{path}..."
535
+ [open_file(path), Mutex.new, finfo.length]
536
+ end
537
+ end
538
+
539
+ i = 0
540
+ @pieces = info.pieces.unpack("a20" * (info.pieces.length / 20)).map do |hash|
541
+ start = (info.piece_length * i)
542
+ len = [info.piece_length, @size - start].min
543
+ p = Piece.new(i, hash, start, len, @files, (created ? false : validity_assumption))
544
+ p.on_event(self, :complete) { send_event(:complete) if complete? }
545
+ yield p if block_given?
546
+ (i += 1) && p
547
+ end
548
+
549
+ reopen_ro if complete?
550
+ end
551
+
552
+ def ro?; @ro; end
553
+ def reopen_ro
554
+ raise "called on incomplete package" unless complete?
555
+ return if @ro
556
+
557
+ rt_debug "reopening all files with mode r"
558
+ @files = @files.map do |fp, mutex, size|
559
+ [fp.reopen(fp.path, "rb"), mutex, size]
560
+ end
561
+ @ro = true
562
+ end
563
+
564
+ def complete?; @pieces.detect { |p| !p.complete? || !p.valid? } == nil; end
565
+
566
+ def bytes_completed
567
+ @pieces.inject(0) { |s, p| s + (p.complete? ? p.length : 0) }
568
+ end
569
+
570
+ def pieces_completed
571
+ @pieces.inject(0) { |s, p| s + (p.complete? ? 1 : 0) }
572
+ end
573
+
574
+ def percent_completed
575
+ 100.0 * pieces_completed.to_f / @pieces.length.to_f
576
+ end
577
+
578
+ def num_pieces; @pieces.length; end
579
+
580
+ def to_s
581
+ "<#{self.class} size #@size>"
582
+ end
583
+
584
+ private
585
+
586
+ def open_file(path)
587
+ begin
588
+ File.open(path, "rb+")
589
+ rescue Errno::ENOENT
590
+ File.open(path, "wb+")
591
+ end
592
+ end
593
+ end
594
+
595
+ end