rbkb 0.6.10
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/History.txt +74 -0
- data/README.rdoc +149 -0
- data/Rakefile +47 -0
- data/bin/b64 +5 -0
- data/bin/bgrep +5 -0
- data/bin/blit +5 -0
- data/bin/c +5 -0
- data/bin/crc32 +5 -0
- data/bin/d64 +5 -0
- data/bin/dedump +5 -0
- data/bin/feed +5 -0
- data/bin/hexify +5 -0
- data/bin/len +5 -0
- data/bin/plugsrv +271 -0
- data/bin/rex +10 -0
- data/bin/rstrings +5 -0
- data/bin/slice +5 -0
- data/bin/telson +5 -0
- data/bin/unhexify +5 -0
- data/bin/urldec +5 -0
- data/bin/urlenc +5 -0
- data/bin/xor +5 -0
- data/cli_usage.rdoc +285 -0
- data/doctor-bag.jpg +0 -0
- data/lib/rbkb.rb +51 -0
- data/lib/rbkb/cli.rb +219 -0
- data/lib/rbkb/cli/b64.rb +35 -0
- data/lib/rbkb/cli/bgrep.rb +86 -0
- data/lib/rbkb/cli/blit.rb +89 -0
- data/lib/rbkb/cli/chars.rb +24 -0
- data/lib/rbkb/cli/crc32.rb +35 -0
- data/lib/rbkb/cli/d64.rb +28 -0
- data/lib/rbkb/cli/dedump.rb +52 -0
- data/lib/rbkb/cli/feed.rb +229 -0
- data/lib/rbkb/cli/hexify.rb +65 -0
- data/lib/rbkb/cli/len.rb +76 -0
- data/lib/rbkb/cli/rstrings.rb +108 -0
- data/lib/rbkb/cli/slice.rb +47 -0
- data/lib/rbkb/cli/telson.rb +87 -0
- data/lib/rbkb/cli/unhexify.rb +50 -0
- data/lib/rbkb/cli/urldec.rb +35 -0
- data/lib/rbkb/cli/urlenc.rb +35 -0
- data/lib/rbkb/cli/xor.rb +43 -0
- data/lib/rbkb/extends.rb +725 -0
- data/lib/rbkb/http.rb +21 -0
- data/lib/rbkb/http/base.rb +172 -0
- data/lib/rbkb/http/body.rb +214 -0
- data/lib/rbkb/http/common.rb +74 -0
- data/lib/rbkb/http/headers.rb +370 -0
- data/lib/rbkb/http/parameters.rb +104 -0
- data/lib/rbkb/http/request.rb +58 -0
- data/lib/rbkb/http/response.rb +86 -0
- data/lib/rbkb/plug.rb +9 -0
- data/lib/rbkb/plug/blit.rb +222 -0
- data/lib/rbkb/plug/cli.rb +83 -0
- data/lib/rbkb/plug/feed_import.rb +74 -0
- data/lib/rbkb/plug/peer.rb +67 -0
- data/lib/rbkb/plug/plug.rb +215 -0
- data/lib/rbkb/plug/proxy.rb +26 -0
- data/lib/rbkb/plug/unix_domain.rb +75 -0
- data/lib_usage.rdoc +176 -0
- data/rbkb.gemspec +38 -0
- data/spec/rbkb_spec.rb +7 -0
- data/spec/spec_helper.rb +16 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/test/test_cli_b64.rb +35 -0
- data/test/test_cli_bgrep.rb +137 -0
- data/test/test_cli_blit.rb +11 -0
- data/test/test_cli_chars.rb +21 -0
- data/test/test_cli_crc32.rb +108 -0
- data/test/test_cli_d64.rb +22 -0
- data/test/test_cli_dedump.rb +118 -0
- data/test/test_cli_feed.rb +11 -0
- data/test/test_cli_helper.rb +96 -0
- data/test/test_cli_hexify.rb +63 -0
- data/test/test_cli_len.rb +96 -0
- data/test/test_cli_rstrings.rb +15 -0
- data/test/test_cli_slice.rb +73 -0
- data/test/test_cli_telson.rb +11 -0
- data/test/test_cli_unhexify.rb +43 -0
- data/test/test_cli_urldec.rb +50 -0
- data/test/test_cli_urlenc.rb +44 -0
- data/test/test_cli_xor.rb +71 -0
- data/test/test_helper.rb +5 -0
- data/test/test_http.rb +27 -0
- data/test/test_http_helper.rb +60 -0
- data/test/test_http_request.rb +136 -0
- data/test/test_http_response.rb +222 -0
- data/test/test_rbkb.rb +19 -0
- metadata +238 -0
data/lib/rbkb/http.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
# ???Why???? would anyone create their own HTTP implementation in ruby with
|
3
|
+
# so many options out there? Short answer: Net:HTTP and others just don't cut
|
4
|
+
# it in lots of edge cases. I needed something I could control completely.
|
5
|
+
|
6
|
+
module Rbkb
|
7
|
+
module Http
|
8
|
+
VERSION = "0.0.3"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'time' # gives us Time.httpdate parser and output methods
|
13
|
+
|
14
|
+
require "rbkb/http/common.rb"
|
15
|
+
require "rbkb/http/base.rb"
|
16
|
+
require "rbkb/http/request.rb"
|
17
|
+
require "rbkb/http/response.rb"
|
18
|
+
require "rbkb/http/headers.rb"
|
19
|
+
require "rbkb/http/body.rb"
|
20
|
+
require "rbkb/http/parameters.rb"
|
21
|
+
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Rbkb::Http
|
2
|
+
|
3
|
+
# A base class containing some common features for Request and Response
|
4
|
+
# objects.
|
5
|
+
#
|
6
|
+
# Don't use this class directly, it's intended for being overridden
|
7
|
+
# from its derived classes or mixins.
|
8
|
+
class Base
|
9
|
+
include CommonInterface
|
10
|
+
|
11
|
+
def self.parse(*args)
|
12
|
+
new(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Initializes a new Base object
|
16
|
+
def initialize(*args)
|
17
|
+
_common_init(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
# This method parses just HTTP message body. Expects body to be split
|
21
|
+
# from the headers before-hand.
|
22
|
+
def capture_body(bstr)
|
23
|
+
self.body ||= default_body_obj
|
24
|
+
@body.capture(bstr)
|
25
|
+
end
|
26
|
+
|
27
|
+
# XXX stub
|
28
|
+
def first_entity
|
29
|
+
@first_entity
|
30
|
+
end
|
31
|
+
|
32
|
+
# XXX stub
|
33
|
+
def first_entity=(f)
|
34
|
+
@first_entity=(f)
|
35
|
+
end
|
36
|
+
|
37
|
+
# This method parses only HTTP response headers. Expects headers to be
|
38
|
+
# split from the body before-hand.
|
39
|
+
def capture_headers(hstr)
|
40
|
+
self.headers ||= default_headers_obj
|
41
|
+
|
42
|
+
if @body and not @body.capture_complete?
|
43
|
+
return
|
44
|
+
elsif @headers.capture_complete?
|
45
|
+
self.first_entity, @headers = default_headers_obj.capture_full_headers(hstr)
|
46
|
+
else
|
47
|
+
@headers.capture(hstr)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This method returns the content length from Headers. This is
|
52
|
+
# mostly useful if you are using a BoundBody object for the body.
|
53
|
+
#
|
54
|
+
# Returns nil if no "Content-Length" is not found.
|
55
|
+
#
|
56
|
+
# The opts parameter :ignore_content_length affects this method and
|
57
|
+
# will cause it always to return nil. This is useful, for example,
|
58
|
+
# for the responses to the HTTP HEAD request method, which return
|
59
|
+
# a Content-Length without actual content.
|
60
|
+
#
|
61
|
+
def content_length(hdrs=@headers)
|
62
|
+
raise "headers is nil?" if not hdrs
|
63
|
+
if( (not @opts[:ignore_content_length]) and
|
64
|
+
hdrs.get_header_value("Content-Length").to_s =~ /^(\d+)$/ )
|
65
|
+
|
66
|
+
$1.to_i
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def attach_new_header(hdr_obj=nil)
|
71
|
+
self.headers = hdr_obj
|
72
|
+
return hdr_obj
|
73
|
+
end
|
74
|
+
|
75
|
+
def attach_new_body(body_obj=nil)
|
76
|
+
self.body = body_obj
|
77
|
+
return body_obj
|
78
|
+
end
|
79
|
+
|
80
|
+
# XXX doc override!
|
81
|
+
def default_headers_obj(*args)
|
82
|
+
Header.new(*args)
|
83
|
+
end
|
84
|
+
|
85
|
+
# XXX doc override!
|
86
|
+
def default_body_obj(*args)
|
87
|
+
Body.new(*args)
|
88
|
+
end
|
89
|
+
|
90
|
+
# This method will non-destructively reset the capture state on this
|
91
|
+
# object and all child entities. Note, however, If child entities are not
|
92
|
+
# defined, it may instantiate new ones.
|
93
|
+
# See also: capture_complete?, reset_capture!
|
94
|
+
def reset_capture
|
95
|
+
if @headers
|
96
|
+
@headers.reset_capture if not @headers.capture_complete?
|
97
|
+
else
|
98
|
+
attach_new_header()
|
99
|
+
end
|
100
|
+
|
101
|
+
if @body
|
102
|
+
@body.reset_capture if not @body.capture_complete?
|
103
|
+
else
|
104
|
+
attach_new_body()
|
105
|
+
end
|
106
|
+
@capture_state = nil
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
# This method will destructively reset the capture state on this object.
|
111
|
+
# It does so by initializing fresh child entities and discarding the old
|
112
|
+
# ones. See also: capture_complete?, reset_capture
|
113
|
+
def reset_capture!
|
114
|
+
attach_new_header()
|
115
|
+
attach_new_body()
|
116
|
+
@capture_state = nil
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
# Indicates whether this object is ready to capture fresh data, or is
|
121
|
+
# waiting for additional data or a reset from a previous incomplete or
|
122
|
+
# otherwise broken capture. See also: reset_capture, reset_capture!
|
123
|
+
def capture_complete?
|
124
|
+
if( (@headers and not @headers.capture_complete?) or
|
125
|
+
(@body and not @body.capture_complete?) )
|
126
|
+
return false
|
127
|
+
else
|
128
|
+
true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
attr_reader :body, :headers
|
133
|
+
|
134
|
+
# This accessor will attempt to always do the "right thing" while
|
135
|
+
# setting this object's body entity.
|
136
|
+
#
|
137
|
+
# See also: default_body_obj
|
138
|
+
def body=(b)
|
139
|
+
if @body
|
140
|
+
@body.data = b
|
141
|
+
elsif b.kind_of? Body
|
142
|
+
@body = b.dup
|
143
|
+
@body.opts = b.opts
|
144
|
+
else
|
145
|
+
@body = default_body_obj(b)
|
146
|
+
end
|
147
|
+
@body.base = self
|
148
|
+
return @body
|
149
|
+
end
|
150
|
+
|
151
|
+
# This accessor will attempt to always do the "right thing" while
|
152
|
+
# setting this object's headers entity.
|
153
|
+
#
|
154
|
+
# See also: default_headers_obj
|
155
|
+
def headers=(h)
|
156
|
+
if @headers
|
157
|
+
@headers.data = h
|
158
|
+
elsif h.kind_of? Headers
|
159
|
+
@headers = h.dup
|
160
|
+
@headers.opts = h.opts
|
161
|
+
else
|
162
|
+
@headers = default_headers_obj(h)
|
163
|
+
end
|
164
|
+
@headers.base = self
|
165
|
+
return @body
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
end
|
172
|
+
|
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Rbkb::Http
|
4
|
+
class Body < String
|
5
|
+
include CommonInterface
|
6
|
+
|
7
|
+
def self.parse(str)
|
8
|
+
new().capture(str)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :expect_length
|
12
|
+
|
13
|
+
def initialize(str=nil, opts=nil)
|
14
|
+
self.opts = opts
|
15
|
+
if Body === str
|
16
|
+
self.replace(str)
|
17
|
+
@opts = str.opts.merge(@opts)
|
18
|
+
elsif String === str
|
19
|
+
super(str)
|
20
|
+
else
|
21
|
+
super()
|
22
|
+
end
|
23
|
+
|
24
|
+
yield(self) if block_given?
|
25
|
+
end
|
26
|
+
|
27
|
+
# The capture method is used when parsing HTTP requests/responses.
|
28
|
+
# This can and probably should be overridden in derived classes.
|
29
|
+
def capture(str)
|
30
|
+
yield(str) if block_given?
|
31
|
+
self.data=(str)
|
32
|
+
end
|
33
|
+
|
34
|
+
# The to_raw method is used when writing HTTP requests/responses.
|
35
|
+
# This can and probably should be overridden in derived classes.
|
36
|
+
def to_raw
|
37
|
+
(block_given?) ? yield(self.data) : self.data
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :base
|
41
|
+
|
42
|
+
def base=(b)
|
43
|
+
if b.nil? or b.is_a? Base
|
44
|
+
@base = b
|
45
|
+
else
|
46
|
+
raise "base must be a Response or Request object or nil"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def data
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Sets internal raw string data without any HTTP decoration.
|
55
|
+
def data=(str)
|
56
|
+
self.replace(str.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the content length from the HTTP base object if
|
60
|
+
# there is one and content-length is available.
|
61
|
+
def get_content_length
|
62
|
+
@base.content_length if @base
|
63
|
+
end
|
64
|
+
|
65
|
+
# This method will non-destructively reset the capture state on this object.
|
66
|
+
# It is non-destructive in that it will not affect existing captured data
|
67
|
+
# if present.
|
68
|
+
def reset_capture
|
69
|
+
@expect_length = nil
|
70
|
+
@base.reset_capture() if @base and @base.capture_complete?
|
71
|
+
end
|
72
|
+
|
73
|
+
# This method will destructively reset the capture state on this object.
|
74
|
+
# This method is destructive in that it will clear any previously captured
|
75
|
+
# data.
|
76
|
+
def reset_capture!
|
77
|
+
reset_capture()
|
78
|
+
self.data=""
|
79
|
+
end
|
80
|
+
|
81
|
+
def capture_complete?
|
82
|
+
not @expect_length
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# BoundBody is designed for handling an HTTP body when using the usual
|
88
|
+
# "Content-Length: NNN" HTTP header.
|
89
|
+
class BoundBody < Body
|
90
|
+
|
91
|
+
# This method may throw :expect_length with one of the following values
|
92
|
+
# to indicate certain content-length conditions:
|
93
|
+
#
|
94
|
+
# > 0 : Got incomplete data in this capture. The object expects
|
95
|
+
# capture to be called again with more body data.
|
96
|
+
#
|
97
|
+
# < 0 : Got more data than expected, the caller should truncate and
|
98
|
+
# handle the extra data in some way. Note: Calling capture again
|
99
|
+
# on this instance will start a fresh body capture.
|
100
|
+
#
|
101
|
+
# Caller can also detect the above conditions by checking the expect_length
|
102
|
+
# attribute but should still be prepared handle the throw().
|
103
|
+
#
|
104
|
+
# 0/nil: Got exactly what was expected. Caller can proceed with fresh
|
105
|
+
# captures on this or other Body objects.
|
106
|
+
#
|
107
|
+
# See also reset_capture and reset_capture!
|
108
|
+
def capture(str)
|
109
|
+
raise "arg 0 must be a string" unless String === str
|
110
|
+
|
111
|
+
# Start fresh unless we're expecting more data
|
112
|
+
self.data="" unless @expect_length and @expect_length > 0
|
113
|
+
|
114
|
+
if not clen=get_content_length()
|
115
|
+
raise "content-length is unknown. aborting capture"
|
116
|
+
else
|
117
|
+
@expect_length = clen - (self.size + str.size)
|
118
|
+
self << str[0, clen - self.size]
|
119
|
+
if @expect_length > 0
|
120
|
+
throw(:expect_length, @expect_length)
|
121
|
+
elsif @expect_length < 0
|
122
|
+
throw(:expect_length, @expect_length)
|
123
|
+
else
|
124
|
+
reset_capture()
|
125
|
+
end
|
126
|
+
end
|
127
|
+
return self
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_raw(*args)
|
131
|
+
if @base
|
132
|
+
@base.headers.set_header("Content-Length", self.size)
|
133
|
+
end
|
134
|
+
super(*args)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
# ChunkedBody is designed for handling an HTTP body when using a
|
140
|
+
# "Transfer-Encoding: chunked" HTTP header.
|
141
|
+
class ChunkedBody < Body
|
142
|
+
DEFAULT_CHUNK_SIZE = 2048
|
143
|
+
|
144
|
+
# Throws :expect_length with 'true' when given incomplete data and expects
|
145
|
+
# to be called again with more body data to parse.
|
146
|
+
#
|
147
|
+
# The caller can also detect this condition by checking the expect_length
|
148
|
+
# attribute but must still handle the throw().
|
149
|
+
#
|
150
|
+
# See also reset_capture and reset_capture!
|
151
|
+
def capture(str)
|
152
|
+
# chunked encoding is gross...
|
153
|
+
if @expect_length
|
154
|
+
sio = StringIO.new(@last_chunk.to_s + str)
|
155
|
+
else
|
156
|
+
sio = StringIO.new(str)
|
157
|
+
self.data=""
|
158
|
+
end
|
159
|
+
@last_chunk = nil
|
160
|
+
|
161
|
+
@expect_length = true
|
162
|
+
while not sio.eof?
|
163
|
+
unless m=/^([a-fA-F0-9]+)\s*(;[[:print:]\s]*)?\r?\n$/.match(line=sio.readline)
|
164
|
+
raise "invalid chunk at #{line.chomp.inspect}"
|
165
|
+
end
|
166
|
+
if (chunksz = m[1].hex) == 0
|
167
|
+
@expect_length = false
|
168
|
+
# XXX ignore Trailer headers
|
169
|
+
break
|
170
|
+
end
|
171
|
+
|
172
|
+
if ( (not sio.eof?) and
|
173
|
+
(chunk=sio.read(chunksz)) and
|
174
|
+
chunk.size == chunksz and
|
175
|
+
(not sio.eof?) and (extra = sio.readline) and
|
176
|
+
(not sio.eof?) and (extra << sio.readline)
|
177
|
+
)
|
178
|
+
if extra =~ /^\r?\n\r?\n$/
|
179
|
+
yield(chunk) if block_given?
|
180
|
+
self << chunk
|
181
|
+
else
|
182
|
+
raise "expected CRLF"
|
183
|
+
end
|
184
|
+
else
|
185
|
+
@last_chunk = line + chunk.to_s + extra.to_s
|
186
|
+
break
|
187
|
+
end
|
188
|
+
end
|
189
|
+
throw(:expect_length, @expect_length) if @expect_length
|
190
|
+
return self
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
def to_raw(csz=nil)
|
195
|
+
csz ||= (@opts[:output_chunk_size] || DEFAULT_CHUNK_SIZE)
|
196
|
+
unless csz.kind_of? Integer and csz > 0
|
197
|
+
raise "chunk size must be an integer >= 1"
|
198
|
+
end
|
199
|
+
|
200
|
+
out=[]
|
201
|
+
i=0
|
202
|
+
while i <= self.size
|
203
|
+
chunk = self[i, csz]
|
204
|
+
out << "#{chunk.size.to_s(16)}\r\n#{chunk}\r\n\r\n"
|
205
|
+
yield(self, out.last) if block_given?
|
206
|
+
i+=csz
|
207
|
+
end
|
208
|
+
out << "0\r\n"
|
209
|
+
yield(self, out.last) if block_given?
|
210
|
+
return out.join
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Rbkb::Http
|
2
|
+
DEFAULT_HTTP_VERSION = "HTTP/1.1"
|
3
|
+
|
4
|
+
module CommonInterface
|
5
|
+
# This provides a common method for use in 'initialize' to slurp in
|
6
|
+
# opts parameters and optionally capture a raw blob. This method also
|
7
|
+
# accepts a block to which it yields 'self'
|
8
|
+
def _common_init(raw=nil, opts=nil)
|
9
|
+
self.opts = opts
|
10
|
+
yield self if block_given?
|
11
|
+
capture(raw) if raw
|
12
|
+
return self
|
13
|
+
end
|
14
|
+
|
15
|
+
# Implements a common interface for an opts hash which is stored internally
|
16
|
+
# as the class variable @opts.
|
17
|
+
#
|
18
|
+
# The opts hash is designed to contain various named values for
|
19
|
+
# configuration, etc. The values and names are determined entirely
|
20
|
+
# by the class that uses it.
|
21
|
+
def opts
|
22
|
+
@opts
|
23
|
+
end
|
24
|
+
|
25
|
+
# Implements a common interface for setting a new opts hash containing
|
26
|
+
# various named values for configuration, etc. This also performs a
|
27
|
+
# minimal sanity check to ensure the object is a Hash.
|
28
|
+
def opts=(o=nil)
|
29
|
+
raise "opts must be a hash" unless (o ||= {}).is_a? Hash
|
30
|
+
@opts = o
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# A generic cheat for an Array of named value pairs to pretend to
|
36
|
+
# be like Hash when using [] and []=
|
37
|
+
class NamedValueArray < Array
|
38
|
+
|
39
|
+
# Act like a hash with named values. Return the named value if a string
|
40
|
+
# or Symbol is supplied as the index argument.
|
41
|
+
#
|
42
|
+
# Note, this doesn't do any magic with String / Symbol conversion.
|
43
|
+
def [](*args)
|
44
|
+
if args.size == 1 and (String === args[0] or Symbol === args[0])
|
45
|
+
if h=find {|x| x[0] == args[0]}
|
46
|
+
return h[1]
|
47
|
+
end
|
48
|
+
else
|
49
|
+
super(*args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Act like a hash with named values. Set the named value if a String
|
54
|
+
# or Symbol is supplied as the index argument.
|
55
|
+
#
|
56
|
+
# Note, this doesn't do any magic with String / Symbol conversion.
|
57
|
+
def []=(*args)
|
58
|
+
if args.size > 1 and (String === args[0] or Symbol === args[0])
|
59
|
+
if h=find {|x| x[0] == args[0]}
|
60
|
+
h[1] = args[1]
|
61
|
+
else
|
62
|
+
self << args[0,2]
|
63
|
+
end
|
64
|
+
else
|
65
|
+
super(*args)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete_key(key)
|
70
|
+
delete_if {|x| x[0] == key }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|