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.
- 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,166 @@
|
|
1
|
+
## server.rb -- make/receive and handshake all new peer connections.
|
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 'socket'
|
15
|
+
require "rubytorrent/tracker"
|
16
|
+
require "rubytorrent/controller"
|
17
|
+
require "rubytorrent/peer"
|
18
|
+
|
19
|
+
module RubyTorrent
|
20
|
+
|
21
|
+
## The Server coordinates all Packages available on the machine. It
|
22
|
+
## instantiates one Controller for each Package. It's also responsible
|
23
|
+
## for the creation of all TCP connections---it sets up the TCP
|
24
|
+
## socket, receives incoming connections, validates handshakes, and
|
25
|
+
## hands them off to the appropriate Controller; it also creates
|
26
|
+
## outgoing connections (typically at Controllers' requests) and sends
|
27
|
+
## the handshake.
|
28
|
+
class Server
|
29
|
+
attr_reader :port, :id, :http_proxy
|
30
|
+
|
31
|
+
VERSION = 0
|
32
|
+
PORT_RANGE=(6881 .. 6889)
|
33
|
+
|
34
|
+
def initialize(hostname=nil, port=nil, http_proxy=ENV["http_proxy"])
|
35
|
+
@http_proxy = http_proxy
|
36
|
+
@server = nil
|
37
|
+
if port.nil?
|
38
|
+
@port = PORT_RANGE.detect do |p|
|
39
|
+
begin
|
40
|
+
@server = TCPServer.new(hostname, p)
|
41
|
+
@port = p
|
42
|
+
rescue Errno::EADDRINUSE
|
43
|
+
@server = nil
|
44
|
+
end
|
45
|
+
!@server.nil?
|
46
|
+
end
|
47
|
+
raise Errno::EADDRINUSE, "ports #{PORT_RANGE}" unless @port
|
48
|
+
else
|
49
|
+
@server = TCPServer.new(hostname, port)
|
50
|
+
@port = port
|
51
|
+
end
|
52
|
+
|
53
|
+
@id = "rubytor" + VERSION.chr + (1 .. 12).map { |x| rand(256).chr }.join
|
54
|
+
@controllers = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def ip; @server.addr[3]; end
|
58
|
+
|
59
|
+
def add_torrent(mi, package, dlratelim=nil, ulratelim=nil)
|
60
|
+
@controllers[mi.info.sha1] = Controller.new(self, package, mi.info.sha1, mi.trackers, dlratelim, ulratelim, @http_proxy)
|
61
|
+
@controllers[mi.info.sha1].start
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_connection(name, cont, socket)
|
65
|
+
begin
|
66
|
+
shake_hands(socket, cont.info_hash)
|
67
|
+
peer = PeerConnection.new(name, cont, socket, cont.package)
|
68
|
+
cont.add_peer peer
|
69
|
+
rescue ProtocolError => e
|
70
|
+
socket.close rescue nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def start
|
75
|
+
@shutdown = false
|
76
|
+
@thread = Thread.new do
|
77
|
+
begin
|
78
|
+
while !@shutdown; receive; end
|
79
|
+
rescue IOError, StandardError
|
80
|
+
rt_warning "**** socket receive error, retrying"
|
81
|
+
sleep 5
|
82
|
+
retry
|
83
|
+
end
|
84
|
+
end
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def shutdown
|
89
|
+
return if @shutdown
|
90
|
+
@shutdown = true
|
91
|
+
@server.close rescue nil
|
92
|
+
|
93
|
+
@thread.join(0.2)
|
94
|
+
@controllers.each { |hash, cont| cont.shutdown }
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
"<#{self.class}: port #{port}, peer_id #{@id.inspect}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def receive # blocking
|
105
|
+
ssocket = @server.accept
|
106
|
+
Thread.new do
|
107
|
+
socket = ssocket
|
108
|
+
|
109
|
+
begin
|
110
|
+
rt_debug "<= incoming connection from #{socket.peeraddr[2]}:#{socket.peeraddr[1]}"
|
111
|
+
hash, peer_id = shake_hands(socket, nil)
|
112
|
+
cont = @controllers[hash]
|
113
|
+
peer = PeerConnection.new("#{socket.peeraddr[2]}:#{socket.peeraddr[1]}", cont, socket, cont.package)
|
114
|
+
cont.add_peer peer
|
115
|
+
rescue SystemCallError, ProtocolError => e
|
116
|
+
rt_debug "killing incoming connection: #{e}"
|
117
|
+
socket.close rescue nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
## if info_hash is nil here, the socket is treated as an incoming
|
123
|
+
## connection---it will wait for the peer's info_hash and respond
|
124
|
+
## with the same if it corresponds to a current download, otherwise
|
125
|
+
## it will raise a ProtocolError.
|
126
|
+
##
|
127
|
+
## if info_hash is not nil, the socket is treated as an outgoing
|
128
|
+
## connection, and it will send the info_hash immediately.
|
129
|
+
def shake_hands(sock, info_hash)
|
130
|
+
# rt_debug "initiating #{(info_hash.nil? ? 'incoming' : 'outgoing')} handshake..."
|
131
|
+
sock.send("\023BitTorrent protocol\0\0\0\0\0\0\0\0", 0);
|
132
|
+
sock.send("#{info_hash}#{@id}", 0) unless info_hash.nil?
|
133
|
+
|
134
|
+
len = sock.recv(1)[0]
|
135
|
+
# rt_debug "length #{len.inspect}"
|
136
|
+
raise ProtocolError, "invalid handshake length byte #{len.inspect}" unless len == 19
|
137
|
+
|
138
|
+
name = sock.recv(19)
|
139
|
+
# rt_debug "name #{name.inspect}"
|
140
|
+
raise ProtocolError, "invalid handshake protocol string #{name.inspect}" unless name == "BitTorrent protocol"
|
141
|
+
|
142
|
+
reserved = sock.recv(8)
|
143
|
+
# rt_debug "reserved: #{reserved.inspect}"
|
144
|
+
# ignore for now
|
145
|
+
|
146
|
+
their_hash = sock.recv(20)
|
147
|
+
# rt_debug "their info hash: #{their_hash.inspect}"
|
148
|
+
|
149
|
+
if info_hash.nil?
|
150
|
+
raise ProtocolError, "client requests package we don't have: hash=#{their_hash.inspect}" unless @controllers.has_key? their_hash
|
151
|
+
info_hash = their_hash
|
152
|
+
sock.send("#{info_hash}#{@id}", 0)
|
153
|
+
else
|
154
|
+
raise ProtocolError, "mismatched info hashes: us=#{info_hash.inspect}, them=#{their_hash.inspect}" unless info_hash == their_hash
|
155
|
+
end
|
156
|
+
|
157
|
+
peerid = sock.recv(20)
|
158
|
+
# rt_debug "peer id: #{peerid.inspect}"
|
159
|
+
raise ProtocolError, "connected to self" if peerid == @id
|
160
|
+
|
161
|
+
# rt_debug "== handshake complete =="
|
162
|
+
[info_hash, peerid]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
## tracker.rb -- bittorrent tracker protocol.
|
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 'open-uri'
|
15
|
+
require 'timeout'
|
16
|
+
require "rubytorrent"
|
17
|
+
|
18
|
+
module RubyTorrent
|
19
|
+
|
20
|
+
module HashAddition
|
21
|
+
def +(o)
|
22
|
+
ret = self.dup
|
23
|
+
o.each { |k, v| ret[k] = v }
|
24
|
+
ret
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
## am i insane or does 'uniq' not use == or === for some insane
|
29
|
+
## reason? wtf is that about?
|
30
|
+
module ArrayUniq2
|
31
|
+
def uniq2
|
32
|
+
ret = []
|
33
|
+
each { |x| ret.push x unless ret.member? x }
|
34
|
+
ret
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class TrackerResponsePeer
|
39
|
+
attr_writer :tried
|
40
|
+
|
41
|
+
def initialize(dict=nil)
|
42
|
+
@s = TypedStruct.new do |s|
|
43
|
+
s.field :peer_id => String, :ip => String, :port => Integer
|
44
|
+
s.required :ip, :port
|
45
|
+
s.label :peer_id => "peer id"
|
46
|
+
end
|
47
|
+
|
48
|
+
@s.parse(dict) unless dict.nil?
|
49
|
+
@connected = false
|
50
|
+
@tried = false
|
51
|
+
end
|
52
|
+
|
53
|
+
def tried?; @tried; end
|
54
|
+
|
55
|
+
def method_missing(meth, *args)
|
56
|
+
@s.send(meth, *args)
|
57
|
+
end
|
58
|
+
|
59
|
+
def ==(o); (self.ip == o.ip) && (self.port == o.port); end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
%{<#{self.class}: ip=#{self.ip}, port=#{self.port}>}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class TrackerResponse
|
67
|
+
def initialize(dict=nil)
|
68
|
+
@s = TypedStruct.new do |s|
|
69
|
+
s.field :interval => Integer, :complete => Integer,
|
70
|
+
:incomplete => Integer, :peers => TrackerResponsePeer
|
71
|
+
s.array :peers
|
72
|
+
s.required :peers #:interval, :complete, :incomplete, :peers
|
73
|
+
s.coerce :peers => lambda { |x| make_peers x }
|
74
|
+
end
|
75
|
+
|
76
|
+
@s.parse(dict) unless dict.nil?
|
77
|
+
|
78
|
+
peers.extend ArrayShuffle
|
79
|
+
end
|
80
|
+
|
81
|
+
def method_missing(meth, *args)
|
82
|
+
@s.send(meth, *args)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def make_peers(x)
|
88
|
+
case x
|
89
|
+
when Array
|
90
|
+
x.map { |e| TrackerResponsePeer.new e }.extend(ArrayUniq2).uniq2
|
91
|
+
when String
|
92
|
+
x.unpack("a6" * (x.length / 6)).map do |y|
|
93
|
+
TrackerResponsePeer.new({"ip" => (0..3).map { |i| y[i] }.join('.'),
|
94
|
+
"port" => (y[4] << 8) + y[5] })
|
95
|
+
end.extend(ArrayUniq2).uniq2
|
96
|
+
else
|
97
|
+
raise "don't know how to make peers array from #{x.class}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class TrackerError < StandardError; end
|
103
|
+
|
104
|
+
class TrackerConnection
|
105
|
+
attr_reader :port, :left, :peer_id, :last_conn_time, :url, :in_force_refresh
|
106
|
+
attr_accessor :uploaded, :downloaded, :left, :numwant
|
107
|
+
|
108
|
+
def initialize(url, info_hash, length, port, peer_id, ip=nil, numwant=50, http_proxy=ENV["http_proxy"])
|
109
|
+
@url = url
|
110
|
+
@hash = info_hash
|
111
|
+
@length = length
|
112
|
+
@port = port
|
113
|
+
@uploaded = @downloaded = @left = 0
|
114
|
+
@ip = ip
|
115
|
+
@numwant = numwant
|
116
|
+
@peer_id = peer_id
|
117
|
+
@http_proxy = http_proxy
|
118
|
+
@state = :stopped
|
119
|
+
@sent_completed = false
|
120
|
+
@last_conn_time = nil
|
121
|
+
@tracker_data = nil
|
122
|
+
@compact = true
|
123
|
+
@in_force_refresh = false
|
124
|
+
end
|
125
|
+
|
126
|
+
def already_completed; @sent_completed = true; end
|
127
|
+
def sent_completed?; @sent_completed; end
|
128
|
+
|
129
|
+
def started
|
130
|
+
return unless @state == :stopped
|
131
|
+
@state = :started
|
132
|
+
@tracker_data = send_tracker "started"
|
133
|
+
self
|
134
|
+
end
|
135
|
+
|
136
|
+
def stopped
|
137
|
+
return unless @state == :started
|
138
|
+
@state = :stopped
|
139
|
+
@tracker_data = send_tracker "stopped"
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
def completed
|
144
|
+
return if @sent_completed
|
145
|
+
@tracker_data = send_tracker "completed"
|
146
|
+
@sent_completed = true
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
def refresh
|
151
|
+
return unless (Time.now - @last_conn_time) >= (interval || 0)
|
152
|
+
@tracker_data = send_tracker nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def force_refresh
|
156
|
+
return if @in_force_refresh
|
157
|
+
@in_force_refresh = true
|
158
|
+
@tracker_data = send_tracker nil
|
159
|
+
@in_force_refresh = false
|
160
|
+
end
|
161
|
+
|
162
|
+
[:interval, :seeders, :leechers, :peers].each do |m|
|
163
|
+
class_eval %{
|
164
|
+
def #{m}
|
165
|
+
if @tracker_data then @tracker_data.#{m} else nil end
|
166
|
+
end
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def send_tracker(event)
|
173
|
+
resp = nil
|
174
|
+
if @compact
|
175
|
+
resp = get_tracker_response({ :event => event, :compact => 1 })
|
176
|
+
if resp["failure reason"]
|
177
|
+
@compact = false
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
resp = get_tracker_response({ :event => event }) unless resp
|
182
|
+
raise TrackerError, "tracker reports error: #{resp['failure reason']}" if resp["failure reason"]
|
183
|
+
|
184
|
+
TrackerResponse.new(resp)
|
185
|
+
end
|
186
|
+
|
187
|
+
def get_tracker_response(opts)
|
188
|
+
target = @url.dup
|
189
|
+
opts.extend HashAddition
|
190
|
+
opts += {:info_hash => @hash, :peer_id => @peer_id,
|
191
|
+
:port => @port, :uploaded => @uploaded, :downloaded => @downloaded,
|
192
|
+
:left => @left, :numwant => @numwant, :ip => @ip}
|
193
|
+
target.query = opts.map do |k, v|
|
194
|
+
unless v.nil?
|
195
|
+
ek = URI.escape(k.to_s) # sigh
|
196
|
+
ev = URI.escape(v.to_s, /[^a-zA-Z0-9]/)
|
197
|
+
"#{ek}=#{ev}"
|
198
|
+
end
|
199
|
+
end.compact.join "&"
|
200
|
+
|
201
|
+
rt_debug "connecting to #{target.to_s} ..."
|
202
|
+
|
203
|
+
ret = nil
|
204
|
+
begin
|
205
|
+
target.open(:proxy => @http_proxy) do |resp|
|
206
|
+
BStream.new(resp).each do |e|
|
207
|
+
if ret.nil?
|
208
|
+
ret = e
|
209
|
+
else
|
210
|
+
raise TrackerError, "don't understand tracker response (too many objects)"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
rescue SocketError, EOFError, OpenURI::HTTPError, RubyTorrent::TrackerError, Timeout::Error, SystemCallError, NoMethodError => e
|
215
|
+
raise TrackerError, e.message
|
216
|
+
end
|
217
|
+
@last_conn_time = Time.now
|
218
|
+
|
219
|
+
raise TrackerError, "empty tracker response" if ret.nil?
|
220
|
+
raise TrackerError, "don't understand tracker response (not a dict)" unless ret.kind_of? ::Hash
|
221
|
+
ret
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
## typedstruct.rb -- type-checking struct, for bencoded objects.
|
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/bencoding"
|
15
|
+
|
16
|
+
module RubyTorrent
|
17
|
+
|
18
|
+
module ArrayToH
|
19
|
+
def to_h
|
20
|
+
inject({}) { |h, (k, v)| h[k] = v; h } # found this neat trick on the internet
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module HashMapHash
|
25
|
+
def map_hash
|
26
|
+
a = map { |k, v| yield k, v }.extend(ArrayToH).to_h
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class TypedStructError < StandardError; end
|
31
|
+
|
32
|
+
## type-checking struct meant for easy translation from and to
|
33
|
+
## bencoded dicts.
|
34
|
+
class TypedStruct
|
35
|
+
attr_accessor :dirty
|
36
|
+
attr_reader :fields # writer below
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@required = {}
|
40
|
+
@label = {}
|
41
|
+
@coerce = {}
|
42
|
+
@field = {}
|
43
|
+
@array = {}
|
44
|
+
@dirty = false
|
45
|
+
@values = {}
|
46
|
+
|
47
|
+
yield self if block_given?
|
48
|
+
|
49
|
+
@field.each do |f, type|
|
50
|
+
@required[f] ||= false
|
51
|
+
@label[f] ||= f.to_s
|
52
|
+
@array[f] ||= false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_missing(meth, *args)
|
57
|
+
if meth.to_s =~ /^(.*?)=$/
|
58
|
+
# p [meth, args]
|
59
|
+
|
60
|
+
f = $1.intern
|
61
|
+
raise ArgumentError, "no such value #{f}" unless @field.has_key? f
|
62
|
+
|
63
|
+
type = @field[f]
|
64
|
+
o = args[0]
|
65
|
+
if @array[f]
|
66
|
+
raise TypeError, "for #{f}, expecting Array, got #{o.class}" unless o.kind_of? ::Array
|
67
|
+
o.each { |e| raise TypeError, "for elements of #{f}, expecting #{type}, got #{e.class}" unless e.kind_of? type }
|
68
|
+
@values[f] = o
|
69
|
+
@dirty = true
|
70
|
+
else
|
71
|
+
raise TypeError, "for #{f}, expecting #{type}, got #{o.class}" unless o.kind_of? type
|
72
|
+
@values[f] = o
|
73
|
+
@dirty = true
|
74
|
+
end
|
75
|
+
else
|
76
|
+
raise ArgumentError, "no such value #{meth}" unless @field.has_key? meth
|
77
|
+
# p [meth, @values[meth]]
|
78
|
+
|
79
|
+
@values[meth]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
[:required, :array].each do |f|
|
84
|
+
class_eval %{
|
85
|
+
def #{f}(*args)
|
86
|
+
args.each do |x|
|
87
|
+
raise %q{unknown field "\#{x}" in #{f} list} unless @field[x]
|
88
|
+
@#{f}[x] = true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
[:field , :label, :coerce].each do |f|
|
95
|
+
class_eval %{
|
96
|
+
def #{f}(hash)
|
97
|
+
hash.each { |k, v| @#{f}[k] = v }
|
98
|
+
end
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
## given a Hash from a bencoded dict, parses it according to the
|
103
|
+
## rules you've set up with field, required, label, etc.
|
104
|
+
def parse(dict)
|
105
|
+
@required.each do |f, reqd|
|
106
|
+
flabel = @label[f]
|
107
|
+
raise TypedStructError, "missing required parameter #{flabel} (dict has #{dict.keys.join(', ')})" if reqd && !(dict.member? flabel)
|
108
|
+
|
109
|
+
if dict.member? flabel
|
110
|
+
v = dict[flabel]
|
111
|
+
if @coerce.member? f
|
112
|
+
v = @coerce[f][v]
|
113
|
+
end
|
114
|
+
if @array[f]
|
115
|
+
raise TypeError, "for #{flabel}, expecting Array, got #{v.class} instead" unless v.kind_of? ::Array
|
116
|
+
end
|
117
|
+
self.send("#{f}=", v)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
## disabled the following line as applications seem to put tons of
|
122
|
+
## weird fields in their .torrent files.
|
123
|
+
# dict.each { |k, v| raise TypedStructError, %{unknown field "#{k}"} unless @field.member?(k.to_sym) || @label.values.member?(k) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def to_bencoding
|
127
|
+
@required.each { |f, reqd| raise ArgumentError, "missing required parameter #{f}" if reqd && self.send(f).nil? }
|
128
|
+
@field.extend(HashMapHash).map_hash { |f, type| [@label[f], self.send(f)] }.to_bencoding
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|