torrenter 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 473fad5c40e464ed097518df8cff579077b34072
4
+ data.tar.gz: 40ff8e80571d14cd92a32f2e7b1bad4658914a36
5
+ SHA512:
6
+ metadata.gz: e2e4ae190b34baed37fd95981f33fc4b839fbdf6fa70381a71c4fcd25d75e23ae693ab9bb715a6c5da62dffddf9e7e33d4391ae3a7281734e09ea2696f4d7a32
7
+ data.tar.gz: 15d2962fb37e1b76c86f8f9cf98a6e01f421cc4c6d97ed56ece7d36077e7bea4bc970cec02ae0a39e640e2647971ee472342c5db91dc4ad7aa1e73b186a3349c
data/bin/torrenter ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'bencode'
4
+ require 'net/http'
5
+ require 'fileutils'
6
+ require 'torrenter'
7
+ file = ARGV[0]
8
+ $data_dump = "#{file}-data"
9
+ Torrenter::Torrent.new.start(file)
data/lib/torrenter.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'torrenter/message/messager'
2
+ require 'torrenter/message/message_types'
3
+ require 'torrenter/peer'
4
+ require 'torrenter/reactor'
5
+ require 'torrenter/udp'
6
+ require 'torrenter/torrent_reader'
7
+ module Torrenter
8
+ class Torrent
9
+ def start(file)
10
+ IO.write($data_dump, '', 0) if !File.exists?($data_dump)
11
+ stream = BEncode.load_file(file)
12
+ peers = Torrenter::TorrentReader.new(stream)
13
+ peers.determine_protocol
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module Torrenter
2
+ PEER_ID = '-MATT16548651231825-'
3
+ BLOCK = 2**14
4
+ KEEP_ALIVE = "\x00\x00\x00\x00"
5
+ INTERESTED = "\x01"
6
+ HANDSHAKE = "T"
7
+ HAVE = "\x04"
8
+ BITFIELD = "\x05"
9
+ PIECE = "\a"
10
+ CHOKE = "\x00"
11
+ PROTOCOL = "\x13BitTorrent protocol\x00\x00\x00\x00\x00\x00\x00\x00"
12
+ end
@@ -0,0 +1,224 @@
1
+ module Torrenter
2
+ # KEEP_ALIVE = "\x00\x00\x00\x00"
3
+ # BLOCK = 2**14
4
+ # these methods get mixed in with the Peer class as a way to help
5
+ # organize and parse the byte-encoded data. The intention is to shorten
6
+ # and shrink the complexity of the Peer class.
7
+
8
+ # the following methods are responsible solely for data retrieval and data transmission
9
+
10
+ def send_data(msg, opts={})
11
+ begin
12
+ Timeout::timeout(2) { @socket.sendmsg_nonblock(msg) }
13
+ rescue Timeout::Error
14
+ ''
15
+ rescue Errno::EADDRNOTAVAIL
16
+ ''
17
+ rescue Errno::ECONNREFUSED
18
+ ''
19
+ rescue Errno::EPIPE
20
+ ''
21
+ end
22
+ end
23
+
24
+ def recv_data(bytes=BLOCK, opts={})
25
+ begin
26
+ if opts[:peek]
27
+ Timeout::timeout(2) { @socket.recv_nonblock(4, Socket::MSG_PEEK) }
28
+ else
29
+ Timeout::timeout(2) { buffer << @socket.recv_nonblock(bytes) }
30
+ end
31
+ rescue Timeout::Error
32
+ ''
33
+ rescue Errno::EADDRNOTAVAIL
34
+ ''
35
+ rescue Errno::ECONNREFUSED
36
+ ''
37
+ rescue Errno::ECONNRESET
38
+ ''
39
+ rescue IO::EAGAINWaitReadable
40
+ ''
41
+ rescue Errno::ETIMEDOUT
42
+ @status = false
43
+ @buffer = ''
44
+ end
45
+ end
46
+
47
+ # the next few methods responsibility is for parsing the buffer.
48
+ # They are responsible for the critical step of analyzing the buffer,
49
+ # checking for message consistency from the peer (and if it fails a test
50
+ # another message is sent out to attempt to fix the missing data)
51
+
52
+
53
+ # parse message will be the gatekeeper. If the buffer is ever low in the READ part
54
+ # of the torrent program, then it knows more data may be required.
55
+ # It will also be responsble for EVERY new message that gets received, whether
56
+ # its in the download sequence of messaging or in the actual handshake, it will
57
+ # control it that way.
58
+
59
+ # for now, I'm assuming that at least the bitfield will be sent in full
60
+
61
+ def parse_bitfield
62
+ len = msg_len
63
+ buffer.slice!(0)
64
+ @piece_index = buffer.slice!(0...len).unpack("B#{sha_list.size}")
65
+ .first.split('')
66
+ .map { |bit| bit == '1' ? :available : :unavailable }
67
+ end
68
+
69
+ def send_interested
70
+ send_data("\x00\x00\x00\x01\x02")
71
+ end
72
+
73
+ def parse_have
74
+ if buffer[5..8].bytesize == 4
75
+ index = buffer[5..8].unpack("N*").first
76
+ @piece_index[index] = :available
77
+ buffer.slice!(0..8)
78
+ else
79
+ recv_data
80
+ end
81
+ send_interested if @buffer.empty?
82
+ end
83
+
84
+ # because ruby.
85
+
86
+ def parse_handshake
87
+ @buffer.slice!(0..67) if hash_match?
88
+ end
89
+
90
+ def hash_match?
91
+ @buffer.unpack("A*").first[28..47] == info_hash
92
+ end
93
+
94
+ # the negative 1 modifier is for factoring in the id
95
+
96
+ def msg_len
97
+ @buffer.slice!(0..3).unpack("N*").first - 1
98
+ end
99
+
100
+ def evaluate_index(master)
101
+ @requesting = 0
102
+ if @piece_index.any? { |chunk| chunk == :available }
103
+ @index = @piece_index.index(:available)
104
+ if master[@index] == :free
105
+ @piece_index[@index] = :downloading
106
+ master[@index] = :downloading
107
+ else
108
+ @piece_index[@index] = :downloaded
109
+ evaluate_index(master)
110
+ end
111
+ end
112
+ end
113
+
114
+ def modify_index(master)
115
+ master.each_with_index do |v,i|
116
+ if v == :downloaded
117
+ @piece_index[i] = :downloaded
118
+ end
119
+ end
120
+ end
121
+
122
+ def state(master, blocks)
123
+ @buffer_state = @buffer[4]
124
+ if buffer.bytesize <= 3
125
+ recv_data
126
+ elsif buffer.bytesize == 4 && buffer[0..3] == KEEP_ALIVE
127
+ buffer.slice!(0..3)
128
+ else
129
+ # p @buffer[0..3].unp@buack("C*")
130
+ case @buffer[4]
131
+ when INTERESTED
132
+ buffer.slice!(0..4)
133
+ modify_index(master)
134
+ evaluate_index(master)
135
+ request_message
136
+ when HAVE
137
+ parse_have
138
+ when BITFIELD
139
+ parse_bitfield
140
+ send_interested if buffer.empty?
141
+ when PIECE
142
+ @length = buffer[0..3].unpack("N*").first + 4
143
+ if buffer.bytesize >= @length
144
+
145
+ buffer.slice!(0..12)
146
+ # the metadata is slireced off.
147
+ @offset += (@length - 13)
148
+ # buffer is reduced
149
+ pack_buffer
150
+ # that means the bytes for that block have been collected entirely.
151
+ # the buffer becomes empty
152
+ if pieces_remaining(master) > 1
153
+ if piece_size == @piece_len
154
+ pack_file(master)
155
+ evaluate_index(master)
156
+ request_message
157
+ else
158
+ request_message
159
+ end
160
+ elsif (File.size($data_dump) + piece_size) == total_file_size
161
+ pack_file(master)
162
+ else
163
+ if bytes_remaining >= BLOCK
164
+ request_message
165
+ else
166
+ request_message(bytes_remaining)
167
+ end
168
+ end
169
+ else
170
+ recv_data(@length - buffer.bytesize)
171
+ end
172
+ # piece
173
+ when CHOKE
174
+ buffer.slice!(0..3)
175
+ send_interested
176
+ when HANDSHAKE
177
+ parse_handshake
178
+ end
179
+ end
180
+ end
181
+
182
+ def pieces_remaining(master)
183
+ master.count(:downloading) + master.count(:free)
184
+ end
185
+
186
+ def bytes_remaining
187
+ (total_file_size - File.size($data_dump)) - piece_size
188
+ end
189
+
190
+ def piece_size
191
+ @block_map.join.bytesize
192
+ end
193
+
194
+ def pack_buffer
195
+ @block_map << @buffer.slice!(0...(@length - 13))
196
+ end
197
+
198
+ def pack_file(master)
199
+ data = @block_map.join
200
+ if piece_verified?(data)
201
+ IO.write($data_dump, data, @index * @offset)
202
+ master[@index] = :downloaded
203
+ @piece_index[@index] = :downloaded
204
+ else
205
+ master[@index] = :free
206
+ @piece_index[@index] = :downloaded
207
+ end
208
+ @block_map = []
209
+ @offset = 0
210
+ end
211
+
212
+ def piece_verified?(data)
213
+ Digest::SHA1.digest(data) == sha_list[@index]
214
+ end
215
+
216
+ def request_message(bytes=BLOCK)
217
+ @requesting += 1
218
+ send_data(pack(13) + "\x06" + pack(@index) + pack(@offset) + pack(bytes))
219
+ end
220
+
221
+ def pack(i)
222
+ [i].pack("I>")
223
+ end
224
+ end
@@ -0,0 +1,62 @@
1
+ # contains the peer information
2
+ module Torrenter
3
+ class Peer
4
+ include Torrenter
5
+ # for non outside vars use the @ and remove them from
6
+
7
+ attr_reader :socket, :peer, :sha_list, :piece_len, :info_hash, :status
8
+ attr_accessor :piece_index, :offset, :buffer, :block_map
9
+
10
+
11
+ def initialize(peer, file_list, peer_info={})
12
+ @peer = peer
13
+ @info_hash = peer_info[:info_hash]
14
+ @piece_len = peer_info[:piece_length]
15
+ @sha_list = peer_info[:sha_list]
16
+ @piece_index = peer_info[:piece_index]
17
+ @buffer = ''
18
+ @block_map = []
19
+ @offset = 0
20
+ @file_list = file_list
21
+ @status = false
22
+ end
23
+
24
+ def total_file_size
25
+ if @file_list.is_a?(Array)
26
+ @file_list.map { |f| f['length'] }.inject { |x, y| x + y }
27
+ else
28
+ @file_list['length']
29
+ end
30
+ end
31
+
32
+ def connect
33
+ puts "\nConnecting to IP: #{peer[:ip]} PORT: #{peer[:port]}"
34
+ begin
35
+ Timeout::timeout(2) { @socket = TCPSocket.new(peer[:ip], peer[:port]) }
36
+ rescue Timeout::Error
37
+ puts "Timed out."
38
+ rescue Errno::EADDRNOTAVAIL
39
+ puts "Address not available."
40
+ rescue Errno::ECONNREFUSED
41
+ puts "Connection refused."
42
+ rescue Errno::ECONNRESET
43
+ puts "bastards."
44
+ end
45
+
46
+ if @socket
47
+ @socket.write(handshake)
48
+ @status = true
49
+ else
50
+ @status = false
51
+ end
52
+ end
53
+
54
+ def handshake
55
+ "#{PROTOCOL}#{@info_hash}#{PEER_ID}"
56
+ end
57
+
58
+ def msg_num
59
+ @buffer.slice!(0..3).unpack("N*").first
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,105 @@
1
+ module Torrenter
2
+ class Reactor
3
+ include Torrenter
4
+ def initialize(peers, sha_list, piece_length, file_list)
5
+ @peers = peers
6
+ @master_index = Array.new(sha_list.size) { :free }
7
+ @blocks = piece_length / BLOCK
8
+ @file_list = file_list
9
+ @piece_length = piece_length
10
+ @sha_list = sha_list
11
+ @data_size = if file_list.is_a?(Array)
12
+ file_list.map { |f| f['length'] }.inject { |x, y| x + y }
13
+ else
14
+ file_list['length']
15
+ end
16
+ end
17
+
18
+ def modify_index
19
+ IO.write($data_dump, '', 0) unless File.exists?($data_dump)
20
+ file_size = File.size($data_dump)
21
+ 0.upto(@sha_list.size - 1) do |n|
22
+ data = IO.read($data_dump, @piece_length, n * @piece_length) || ''
23
+ @master_index[n] = :downloaded if Digest::SHA1.digest(data) == @sha_list[n]
24
+ end
25
+ puts "#{@master_index.count(:downloaded)} pieces are downloaded already."
26
+ end
27
+
28
+ # sends the handshake messages.
29
+
30
+ def message_reactor(opts={})
31
+ modify_index
32
+ if !@master_index.all? { |index| index == :downloaded }
33
+ @peers.each { |peer| peer.connect }
34
+ loop do
35
+ break if @master_index.all? { |piece| piece == :downloaded }
36
+ @peers.each do |peer|
37
+ piece_count = @master_index.count(:downloaded)
38
+ if peer.status
39
+ peer.state(@master_index, @blocks) # unless peer.piece_index.all? { |piece| piece == :downloaded }
40
+ if @master_index.count(:downloaded) > piece_count
41
+ system("clear")
42
+ puts download_bar + "Downloading from #{active_peers} active peers"
43
+ end
44
+ elsif Time.now.to_i % 60 == 0
45
+ peer.connect
46
+ end
47
+ end
48
+ end
49
+ stop_downloading
50
+ seperate_data_dump_into_files
51
+ else
52
+ upload_data
53
+ end
54
+ end
55
+
56
+ def active_peers
57
+ @peers.select { |peer| peer.status }.size
58
+ end
59
+
60
+ def download_bar
61
+ ("\u2588" * pieces(:downloaded)) + ("\u2593" * pieces(:downloading)) + (" " * pieces(:free)) + " %#{pieces(:downloaded)} downloaded "
62
+ end
63
+
64
+ def pieces(type)
65
+ (@master_index.count(type).fdiv(@master_index.size) * 100).round
66
+ end
67
+
68
+ def stop_downloading
69
+ @peers.each { |peer| peer.piece_index.map { |piece| piece = :downloaded} }
70
+ end
71
+
72
+ def seperate_data_dump_into_files
73
+ if multiple_files?
74
+ offset = 0
75
+ folder = $data_dump[/.+(?=\.torrent-data)/] || FileUtils.mkdir($data_dump[/.+(?=\.torrent-data)/]).join
76
+ @file_list.each do |file|
77
+
78
+ length = file['length']
79
+ filename = file['path'].pop
80
+
81
+ if multiple_sub_folders?(file)
82
+ subfolders = file['path'].join("/")
83
+ folder = folder + "/" + subfolders
84
+ FileUtils.mkdir_p("#{folder}", force: true)
85
+ end
86
+
87
+ File.open("#{folder}/#{filename}", 'a+') { |data| data << IO.read($data_dump, length, offset) }
88
+
89
+ offset += length
90
+ end
91
+ else
92
+ File.open("#{folder}/#{@file_list['name'].join}", 'w') { |data| data << File.read($data_dump) }
93
+ end
94
+ File.delete($data_dump)
95
+ end
96
+
97
+ def multiple_sub_folders?(file)
98
+ file['path'].size > 1
99
+ end
100
+
101
+ def multiple_files?
102
+ @file_list.is_a?(Array)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,137 @@
1
+ require 'socket'
2
+ require 'digest/sha1'
3
+ require 'bencode'
4
+ require 'pry'
5
+ require 'fileutils'
6
+
7
+
8
+ module Torrenter
9
+ class TorrentReader
10
+ attr_reader :stream
11
+ def initialize(stream)
12
+ @stream = stream
13
+ end
14
+
15
+ def determine_protocol
16
+ trackers = if @stream['announce-list']
17
+ @stream['announce-list'].flatten << @stream['announce']
18
+ else
19
+ [@stream['announce']]
20
+ end
21
+
22
+ @http_trackers = trackers.select { |tracker| tracker =~ /http\:\/\// }
23
+ @udp_trackers = trackers.select { |tracker| tracker =~ /udp\:\/\// }
24
+ # first try the http trackers...
25
+
26
+ connect_to_http_tracker if @http_trackers.size > 0
27
+ connect_to_udp_tracker if @peers.nil?
28
+ peer_list
29
+ establish_reactor
30
+
31
+ end
32
+
33
+ def connect_to_http_tracker
34
+ @uri = URI(@http_trackers.shift)
35
+ @uri.query = URI.encode_www_form(peer_hash)
36
+ @peers = raw_peers
37
+ end
38
+
39
+ def connect_to_udp_tracker
40
+ @udp_trackers.map! do |udp|
41
+ port = udp[/\d+/]
42
+ udp.gsub!(/udp\:\/\/|\:\d+|\/announce/, '')
43
+ begin
44
+ ip = Socket.getaddrinfo(udp, 80)[0][3]
45
+ udp_socket = UDPConnection.new(ip, port.to_i, sha)
46
+ udp_socket.connect_to_udp_host
47
+ rescue SocketError
48
+ puts 'Unuseable UDP site'
49
+ end
50
+ end
51
+
52
+ while @peers.nil?
53
+ udp = @udp_trackers.shift
54
+ begin
55
+ @peers = udp.message_relay
56
+ rescue
57
+ end
58
+
59
+ end
60
+ end
61
+
62
+
63
+ # url gets reformatted to include query parameters
64
+
65
+ def raw_peers
66
+ begin
67
+ BEncode.load(Net::HTTP.get(@uri))['peers']
68
+ rescue
69
+ nil
70
+ end
71
+ end
72
+
73
+
74
+ def piece_length
75
+ stream['info']['piece length']
76
+ end
77
+
78
+ def sha
79
+ Digest::SHA1.digest(stream['info'].bencode)
80
+ end
81
+
82
+ # data stored as a hash in the order made necessary
83
+
84
+ def peer_hash
85
+ {
86
+ :info_hash => sha,
87
+ :peer_id => PEER_ID,
88
+ :left => piece_length,
89
+ :pieces => stream['info']['files'] || stream['info']['name']
90
+ }
91
+ end
92
+
93
+ # Using the peers key of the torrent file, the hex-encoded data gets reinterpreted as ips addresses.
94
+
95
+ def peer_list
96
+ ip_list = []
97
+ until @peers.empty?
98
+ ip_list << { ip: @peers.slice!(0..3).bytes.join('.'), port: @peers.slice!(0..1).unpack("S>").first }
99
+ end
100
+
101
+ @peers = ip_list.map { |peer| Peer.new(peer, file_list, peer_info) }
102
+ end
103
+
104
+ def sha_list
105
+ n, e = 0, 20
106
+ list = []
107
+ until stream['info']['pieces'].bytesize < e
108
+ list << stream['info']['pieces'].byteslice(n...e)
109
+ n += 20
110
+ e += 20
111
+ end
112
+ list
113
+ end
114
+
115
+ def peer_info
116
+ {
117
+ :info_hash => sha,
118
+ :piece_length => piece_length,
119
+ :sha_list => sha_list,
120
+ :piece_index => Array.new(sha_list.size) { false }
121
+ }
122
+ end
123
+
124
+ def file_list
125
+ stream['info']['files'] || stream['info']
126
+ end
127
+
128
+ def establish_reactor
129
+ react = Reactor.new(@peers, sha_list, piece_length, file_list)
130
+ begin
131
+ react.message_reactor
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+
@@ -0,0 +1,87 @@
1
+ module Torrenter
2
+ class UDPConnection
3
+ attr_reader :sender, :response
4
+ def initialize(ip, port, info_hash)
5
+ @ip = ip
6
+ @port = port
7
+ @sender = UDPSocket.new
8
+ @t = rand(10000)
9
+ @connection_id = [0x41727101980].pack("Q>")
10
+ @transaction_id = [@t].pack("I>")
11
+ @info_hash = info_hash
12
+ end
13
+
14
+ def connect_to_udp_host
15
+ begin
16
+ @sender.connect(@ip, @port)
17
+ return self
18
+ rescue
19
+ false
20
+ end
21
+ end
22
+
23
+ def message_relay
24
+ @sender.send(connect_msg, 0)
25
+ read_response
26
+ if @response
27
+ @connection_id = @response[-8..-1]
28
+ @transaction_id = [rand(10000)].pack("I>")
29
+ @sender.send(announce_msg, 0)
30
+ end
31
+
32
+ read_response
33
+
34
+ if @response
35
+ parse_announce if @response[0..3] == action(1)
36
+ return @response
37
+ end
38
+ end
39
+
40
+ def parse_announce
41
+ if @response[4..7] == @transaction_id
42
+ res = @response.slice!(0..11)
43
+ leechers = @response.slice!(0..3).unpack("I>").first
44
+ seeders = @response.slice!(0..3).unpack("I>").first
45
+ end
46
+ end
47
+
48
+ def send_message
49
+ begin
50
+ @sender.send(@msg, 0)
51
+ rescue
52
+ end
53
+ end
54
+
55
+ def read_response
56
+ begin
57
+ @response = @sender.recv(1028)
58
+ rescue Exception => e
59
+ e
60
+ end
61
+ end
62
+
63
+ def connect_match?
64
+ data[0] == (action(0) + @transaction_id + @connection_id)
65
+ end
66
+
67
+ def announce_input
68
+ @connection_id + action(1) + @transaction_id + @info_hash + PEER_ID
69
+ end
70
+
71
+ def connect_msg
72
+ @connection_id + action(0) + @transaction_id
73
+ end
74
+
75
+ def port
76
+ @sender.addr[1]
77
+ end
78
+
79
+ def action(n)
80
+ [n].pack("I>")
81
+ end
82
+
83
+ def announce_msg
84
+ @connection_id + action(1) + @transaction_id + @info_hash + PEER_ID + [0].pack("Q>") + [0].pack("Q>") + [0].pack("Q>") + action(0) + action(0) + action(0) + action(-1) + [port].pack(">S")
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module Torrenter
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: torrenter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - wismer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: BitTorrent Client written in Ruby
14
+ email:
15
+ - matthewhl@gmail.com
16
+ executables:
17
+ - torrenter
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/torrenter
22
+ - lib/torrenter.rb
23
+ - lib/torrenter/message/message_types.rb
24
+ - lib/torrenter/message/messager.rb
25
+ - lib/torrenter/peer.rb
26
+ - lib/torrenter/reactor.rb
27
+ - lib/torrenter/torrent_reader.rb
28
+ - lib/torrenter/udp.rb
29
+ - lib/torrenter/version.rb
30
+ homepage: http://wismer.github.io
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.2.2
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Load by typing the torrent file name after torrenter
54
+ test_files: []