rubytorrent 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +340 -0
- data/README +21 -0
- data/ReleaseNotes.txt +25 -0
- data/doc/api.txt +289 -0
- data/doc/design.txt +59 -0
- data/dump-metainfo.rb +55 -0
- data/dump-peers.rb +45 -0
- data/lib/rubytorrent.rb +94 -0
- data/lib/rubytorrent/bencoding.rb +174 -0
- data/lib/rubytorrent/controller.rb +610 -0
- data/lib/rubytorrent/message.rb +128 -0
- data/lib/rubytorrent/metainfo.rb +214 -0
- data/lib/rubytorrent/package.rb +595 -0
- data/lib/rubytorrent/peer.rb +536 -0
- data/lib/rubytorrent/server.rb +166 -0
- data/lib/rubytorrent/tracker.rb +225 -0
- data/lib/rubytorrent/typedstruct.rb +132 -0
- data/lib/rubytorrent/util.rb +186 -0
- data/make-metainfo.rb +211 -0
- data/rtpeer-ncurses.rb +340 -0
- data/rtpeer.rb +125 -0
- metadata +78 -0
@@ -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
|