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