packetgen 3.1.4 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/bin/pgconsole +1 -0
- data/lib/packetgen.rb +19 -3
- data/lib/packetgen/capture.rb +30 -9
- data/lib/packetgen/config.rb +15 -9
- data/lib/packetgen/deprecation.rb +1 -1
- data/lib/packetgen/header/asn1_base.rb +19 -9
- data/lib/packetgen/header/base.rb +68 -70
- data/lib/packetgen/header/dhcpv6/duid.rb +3 -1
- data/lib/packetgen/header/dhcpv6/option.rb +4 -12
- data/lib/packetgen/header/dns/name.rb +18 -7
- data/lib/packetgen/header/dns/qdsection.rb +1 -1
- data/lib/packetgen/header/dns/question.rb +2 -0
- data/lib/packetgen/header/dot11.rb +25 -38
- data/lib/packetgen/header/dot11/data.rb +28 -34
- data/lib/packetgen/header/dot1x.rb +1 -14
- data/lib/packetgen/header/eap.rb +14 -17
- data/lib/packetgen/header/eth.rb +5 -6
- data/lib/packetgen/header/http/headers.rb +4 -2
- data/lib/packetgen/header/http/request.rb +37 -18
- data/lib/packetgen/header/http/response.rb +11 -5
- data/lib/packetgen/header/http/verbs.rb +1 -1
- data/lib/packetgen/header/igmpv3/group_record.rb +2 -0
- data/lib/packetgen/header/ip.rb +27 -26
- data/lib/packetgen/header/ip/addr.rb +3 -1
- data/lib/packetgen/header/ip/option.rb +4 -4
- data/lib/packetgen/header/ipv6/addr.rb +2 -0
- data/lib/packetgen/header/mldv2/mcast_address_record.rb +2 -0
- data/lib/packetgen/header/ospfv2/ls_request.rb +2 -0
- data/lib/packetgen/header/ospfv2/lsa.rb +13 -3
- data/lib/packetgen/header/ospfv2/lsa_header.rb +2 -1
- data/lib/packetgen/header/ospfv3/ipv6_prefix.rb +2 -0
- data/lib/packetgen/header/ospfv3/ls_request.rb +2 -0
- data/lib/packetgen/header/ospfv3/lsa.rb +9 -3
- data/lib/packetgen/header/ospfv3/lsa_header.rb +2 -1
- data/lib/packetgen/header/snmp.rb +3 -2
- data/lib/packetgen/header/tcp.rb +1 -20
- data/lib/packetgen/header/tcp/option.rb +8 -6
- data/lib/packetgen/inspect.rb +1 -17
- data/lib/packetgen/packet.rb +10 -6
- data/lib/packetgen/pcapng.rb +11 -11
- data/lib/packetgen/pcapng/block.rb +15 -2
- data/lib/packetgen/pcapng/epb.rb +22 -15
- data/lib/packetgen/pcapng/file.rb +166 -81
- data/lib/packetgen/pcapng/idb.rb +7 -9
- data/lib/packetgen/pcapng/shb.rb +35 -28
- data/lib/packetgen/pcapng/spb.rb +16 -12
- data/lib/packetgen/pcapng/unknown_block.rb +3 -11
- data/lib/packetgen/pcaprub_wrapper.rb +25 -11
- data/lib/packetgen/types.rb +1 -0
- data/lib/packetgen/types/abstract_tlv.rb +3 -1
- data/lib/packetgen/types/array.rb +17 -10
- data/lib/packetgen/types/cstring.rb +56 -19
- data/lib/packetgen/types/enum.rb +4 -0
- data/lib/packetgen/types/fieldable.rb +65 -0
- data/lib/packetgen/types/fields.rb +180 -113
- data/lib/packetgen/types/int.rb +15 -1
- data/lib/packetgen/types/int_string.rb +8 -0
- data/lib/packetgen/types/length_from.rb +18 -10
- data/lib/packetgen/types/oui.rb +2 -0
- data/lib/packetgen/types/string.rb +58 -7
- data/lib/packetgen/types/tlv.rb +2 -0
- data/lib/packetgen/unknown_packet.rb +84 -0
- data/lib/packetgen/utils.rb +6 -7
- data/lib/packetgen/version.rb +1 -1
- metadata +18 -15
@@ -87,15 +87,21 @@ module PacketGen
|
|
87
87
|
end
|
88
88
|
unless headers.empty?
|
89
89
|
first_line = headers.shift.split
|
90
|
-
|
91
|
-
|
92
|
-
|
90
|
+
if first_line.size >= 3
|
91
|
+
self[:version].read first_line[0]
|
92
|
+
self[:status_code].read first_line[1]
|
93
|
+
self[:status_mesg].read first_line[2..-1].join(' ')
|
94
|
+
end
|
93
95
|
self[:headers].read(headers.join("\n"))
|
94
96
|
end
|
95
97
|
self[:body].read data.join("\n")
|
96
98
|
self
|
97
99
|
end
|
98
100
|
|
101
|
+
def parse?
|
102
|
+
version.start_with?('HTTP/1.')
|
103
|
+
end
|
104
|
+
|
99
105
|
# String representation of data.
|
100
106
|
# @return [String]
|
101
107
|
def to_s
|
@@ -104,7 +110,7 @@ module PacketGen
|
|
104
110
|
raise FormatError, 'Missing #version.' if self.version.empty?
|
105
111
|
|
106
112
|
str = +''
|
107
|
-
str << self
|
113
|
+
str << self.version << ' ' << self.status_code << ' ' << self.status_mesg << "\r\n"
|
108
114
|
str << self[:headers].to_s if self[:headers].given?
|
109
115
|
str << self.body
|
110
116
|
end
|
@@ -112,6 +118,6 @@ module PacketGen
|
|
112
118
|
end
|
113
119
|
|
114
120
|
self.add_class HTTP::Response
|
115
|
-
TCP.bind HTTP::Response, body: ->(b) { %r[^HTTP/1\.1\s\d{3,}\s.+] =~ b
|
121
|
+
TCP.bind HTTP::Response, body: ->(b) { b.nil? ? '' : %r[^HTTP/1\.1\s\d{3,}\s.+] =~ b }
|
116
122
|
end
|
117
123
|
end
|
@@ -16,7 +16,7 @@ module PacketGen
|
|
16
16
|
VERBS = %w[GET HEAD POST PUT DELETE CONNECT OPTIONS TRACE PATCH].freeze
|
17
17
|
|
18
18
|
# Identifiable HTTP request regular expression.
|
19
|
-
REQUEST_REGEX = Regexp.new(
|
19
|
+
REQUEST_REGEX = Regexp.new("^(#{VERBS.dup.join('|')})\\s+\\S+\\s+HTTP/1.1")
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
data/lib/packetgen/header/ip.rb
CHANGED
@@ -131,7 +131,7 @@ module PacketGen
|
|
131
131
|
# @since 2.2.0
|
132
132
|
# @return [Types::String]
|
133
133
|
define_field :options, Options, optional: ->(h) { h.ihl > 5 },
|
134
|
-
|
134
|
+
builder: ->(h, t) { t.new(length_from: -> { (h.ihl - 5) * 4 }) }
|
135
135
|
# @!attribute body
|
136
136
|
# @return [Types::String,Header::Base]
|
137
137
|
define_field :body, Types::String
|
@@ -166,7 +166,7 @@ module PacketGen
|
|
166
166
|
|
167
167
|
data = hdr.to_s
|
168
168
|
data << "\x00" if data.size.odd?
|
169
|
-
sum = data.unpack('n*').
|
169
|
+
sum = data.unpack('n*').sum
|
170
170
|
|
171
171
|
hdr.checksum = old_checksum if old_checksum
|
172
172
|
|
@@ -191,17 +191,10 @@ module PacketGen
|
|
191
191
|
# @return [Integer]
|
192
192
|
def calc_checksum
|
193
193
|
# Checksum is only on header, so cannot use IP.sum16,
|
194
|
-
# which also
|
195
|
-
|
196
|
-
checksum
|
197
|
-
checksum
|
198
|
-
checksum += self.frag
|
199
|
-
checksum += (self.ttl << 8) | self.protocol
|
200
|
-
checksum += (self[:src].to_i >> 16)
|
201
|
-
checksum += (self[:src].to_i & 0xffff)
|
202
|
-
checksum += self[:dst].to_i >> 16
|
203
|
-
checksum += self[:dst].to_i & 0xffff
|
204
|
-
options.to_s.unpack('n*').each { |x| checksum += x }
|
194
|
+
# which also calculates checksum on #body.
|
195
|
+
nb_words = ihl * 2
|
196
|
+
self.checksum = 0
|
197
|
+
checksum = to_s.unpack("n#{nb_words}").sum
|
205
198
|
self[:checksum].value = IP.reduce_checksum(checksum)
|
206
199
|
end
|
207
200
|
|
@@ -238,20 +231,9 @@ module PacketGen
|
|
238
231
|
super do |attr|
|
239
232
|
case attr
|
240
233
|
when :u8
|
241
|
-
|
242
|
-
str = Inspect.inspect_attribute(attr, self[attr])
|
243
|
-
str << shift << Inspect::FMT_ATTR % ['', 'version', version]
|
244
|
-
str << shift << Inspect::FMT_ATTR % ['', 'ihl', ihl]
|
234
|
+
inspect_u8
|
245
235
|
when :frag
|
246
|
-
|
247
|
-
str = Inspect.inspect_attribute(attr, self[attr])
|
248
|
-
flags = flag_rsv? ? %w[RSV] : []
|
249
|
-
flags << 'DF' if flag_df?
|
250
|
-
flags << 'MF' if flag_mf?
|
251
|
-
flags_str = flags.empty? ? 'none' : flags.join(',')
|
252
|
-
str << shift << Inspect::FMT_ATTR % ['', 'flags', flags_str]
|
253
|
-
foff = Inspect.int_dec_hex(fragment_offset, 4)
|
254
|
-
str << shift << Inspect::FMT_ATTR % ['', 'frag_offset', foff]
|
236
|
+
inspect_frag
|
255
237
|
end
|
256
238
|
end
|
257
239
|
end
|
@@ -276,6 +258,25 @@ module PacketGen
|
|
276
258
|
self[:src], self[:dst] = self[:dst], self[:src]
|
277
259
|
self
|
278
260
|
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
def inspect_u8
|
265
|
+
shift = Inspect.shift_level
|
266
|
+
str = Inspect.inspect_attribute(:u8, self[:u8])
|
267
|
+
str << shift << Inspect::FMT_ATTR % ['', 'version', version]
|
268
|
+
str << shift << Inspect::FMT_ATTR % ['', 'ihl', ihl]
|
269
|
+
end
|
270
|
+
|
271
|
+
def inspect_frag
|
272
|
+
shift = Inspect.shift_level
|
273
|
+
str = Inspect.inspect_attribute(:frag, self[:frag])
|
274
|
+
flags = %i[rsv df mf].select { |flag| send("flag_#{flag}?") }.map(&:upcase)
|
275
|
+
flags_str = flags.empty? ? 'none' : flags.join(',')
|
276
|
+
str << shift << Inspect::FMT_ATTR % ['', 'flags', flags_str]
|
277
|
+
foff = Inspect.int_dec_hex(fragment_offset, 4)
|
278
|
+
str << shift << Inspect::FMT_ATTR % ['', 'frag_offset', foff]
|
279
|
+
end
|
279
280
|
end
|
280
281
|
|
281
282
|
self.add_class IP
|
@@ -11,6 +11,8 @@ module PacketGen
|
|
11
11
|
# IP address, as a group of 4 bytes
|
12
12
|
# @author Sylvain Daubert
|
13
13
|
class Addr < Types::Fields
|
14
|
+
include Types::Fieldable
|
15
|
+
|
14
16
|
# @!attribute a1
|
15
17
|
# @return [Integer] IP address first byte
|
16
18
|
define_field :a1, Types::Int8
|
@@ -24,7 +26,7 @@ module PacketGen
|
|
24
26
|
# @return [Integer] IP address fourth byte
|
25
27
|
define_field :a4, Types::Int8
|
26
28
|
|
27
|
-
IPV4_ADDR_REGEX = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})
|
29
|
+
IPV4_ADDR_REGEX = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.freeze
|
28
30
|
|
29
31
|
# Read a dotted address
|
30
32
|
# @param [String] str
|
@@ -26,6 +26,8 @@ module PacketGen
|
|
26
26
|
# Base class for IP options
|
27
27
|
# @author Sylvain Daubert
|
28
28
|
class Option < Types::Fields
|
29
|
+
include Types::Fieldable
|
30
|
+
|
29
31
|
# EOL option type
|
30
32
|
EOL_TYPE = 0x00
|
31
33
|
# NOP option type
|
@@ -115,10 +117,8 @@ module PacketGen
|
|
115
117
|
# Get a human readable string
|
116
118
|
# @return [String]
|
117
119
|
def to_human
|
118
|
-
str = self.
|
119
|
-
if respond_to?(:length) && (length > 2) && !self[:data].to_s.empty?
|
120
|
-
str << ":#{self[:data].to_s.inspect}"
|
121
|
-
end
|
120
|
+
str = self.instance_of?(Option) ? +"unk-#{type}" : self.class.to_s.sub(/.*::/, '')
|
121
|
+
str << ":#{self[:data].to_s.inspect}" if respond_to?(:length) && (length > 2) && !self[:data].to_s.empty?
|
122
122
|
str
|
123
123
|
end
|
124
124
|
end
|
@@ -52,6 +52,8 @@ module PacketGen
|
|
52
52
|
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
53
53
|
# @author Sylvain Daubert
|
54
54
|
class McastAddressRecord < Types::Fields
|
55
|
+
include Types::Fieldable
|
56
|
+
|
55
57
|
# Known record types
|
56
58
|
RECORD_TYPES = IGMPv3::GroupRecord::RECORD_TYPES
|
57
59
|
|
@@ -23,6 +23,8 @@ module PacketGen
|
|
23
23
|
# LSA router payload}.
|
24
24
|
# @author Sylvain Daubert
|
25
25
|
class TosMetric < Types::Fields
|
26
|
+
include Types::Fieldable
|
27
|
+
|
26
28
|
# @!attribute tos
|
27
29
|
# 8-bit IP Type of Service that this metric refers to.
|
28
30
|
# @return [Integer]
|
@@ -52,6 +54,8 @@ module PacketGen
|
|
52
54
|
# This class handles links in a {LSARouter LSA router payload}.
|
53
55
|
# @author Sylvain Daubert
|
54
56
|
class Link < Types::Fields
|
57
|
+
include Types::Fieldable
|
58
|
+
|
55
59
|
# @!attribute id
|
56
60
|
# @return [IP::Addr]
|
57
61
|
define_field :id, IP::Addr
|
@@ -136,6 +140,8 @@ module PacketGen
|
|
136
140
|
# This class handles external links in {LSAASExternal LSA AS-External payloads}.
|
137
141
|
# @author Sylvain Daubert
|
138
142
|
class External < Types::Fields
|
143
|
+
include Types::Fieldable
|
144
|
+
|
139
145
|
# @!attribute u8
|
140
146
|
# @return [Integer]
|
141
147
|
define_field :u8, Types::Int8
|
@@ -223,9 +229,13 @@ module PacketGen
|
|
223
229
|
|
224
230
|
def get_lsa_class_by_human_type(htype)
|
225
231
|
klassname = "LSA#{htype.to_s.delete('-')}"
|
226
|
-
|
227
|
-
OSPFv2.
|
228
|
-
|
232
|
+
begin
|
233
|
+
if OSPFv2.const_defined? klassname
|
234
|
+
OSPFv2.const_get klassname
|
235
|
+
else
|
236
|
+
LSA
|
237
|
+
end
|
238
|
+
rescue NameError
|
229
239
|
LSA
|
230
240
|
end
|
231
241
|
end
|
@@ -29,6 +29,8 @@ module PacketGen
|
|
29
29
|
# is also a base class for different LSA class, as {LSARouter}.
|
30
30
|
# @author Sylvain Daubert
|
31
31
|
class LSAHeader < Types::Fields
|
32
|
+
include Types::Fieldable
|
33
|
+
|
32
34
|
# LSA Types
|
33
35
|
TYPES = {
|
34
36
|
'Router' => 1,
|
@@ -75,7 +77,6 @@ module PacketGen
|
|
75
77
|
# Compute and set Fletcher-16 checksum on LSA
|
76
78
|
# @return [Integer]
|
77
79
|
def calc_checksum
|
78
|
-
self.checksum = 0
|
79
80
|
bytes = to_s[2..-1].unpack('C*')
|
80
81
|
|
81
82
|
c0 = c1 = 0
|
@@ -11,6 +11,8 @@ module PacketGen
|
|
11
11
|
# This class handles links in a {LSARouter OSPFv3 LSA router payload}.
|
12
12
|
# @author Sylvain Daubert
|
13
13
|
class Link < Types::Fields
|
14
|
+
include Types::Fieldable
|
15
|
+
|
14
16
|
# @!attribute type
|
15
17
|
# @return [Integer]
|
16
18
|
define_field :type, Types::Int8
|
@@ -216,9 +218,13 @@ module PacketGen
|
|
216
218
|
|
217
219
|
def get_lsa_class_by_human_type(htype)
|
218
220
|
klassname = "LSA#{htype.to_s.delete('-')}"
|
219
|
-
|
220
|
-
OSPFv3.
|
221
|
-
|
221
|
+
begin
|
222
|
+
if OSPFv3.const_defined? klassname
|
223
|
+
OSPFv3.const_get klassname
|
224
|
+
else
|
225
|
+
LSA
|
226
|
+
end
|
227
|
+
rescue NameError
|
222
228
|
LSA
|
223
229
|
end
|
224
230
|
end
|
@@ -29,6 +29,8 @@ module PacketGen
|
|
29
29
|
# But this class is also a base class for different LSA class, as {LSARouter}.
|
30
30
|
# @author Sylvain Daubert
|
31
31
|
class LSAHeader < Types::Fields
|
32
|
+
include Types::Fieldable
|
33
|
+
|
32
34
|
# LSA known types
|
33
35
|
TYPES = {
|
34
36
|
'Router' => 0x2001,
|
@@ -76,7 +78,6 @@ module PacketGen
|
|
76
78
|
# Compute and set Fletcher-16 checksum on LSA
|
77
79
|
# @return [Integer]
|
78
80
|
def calc_checksum
|
79
|
-
self.checksum = 0
|
80
81
|
bytes = to_s[2..-1].unpack('C*')
|
81
82
|
|
82
83
|
c0 = c1 = 0
|
@@ -246,7 +246,8 @@ module PacketGen
|
|
246
246
|
end
|
247
247
|
|
248
248
|
sequence :message,
|
249
|
-
content: [integer(:version,
|
249
|
+
content: [integer(:version,
|
250
|
+
value: 'v2c',
|
250
251
|
enum: { 'v1' => 0, 'v2c' => 1, 'v2' => 2, 'v3' => 3 }),
|
251
252
|
octet_string(:community, value: 'public'),
|
252
253
|
model(:data, PDUs)]
|
@@ -295,7 +296,7 @@ module PacketGen
|
|
295
296
|
begin
|
296
297
|
str << Inspect.inspect_body(self[:message].to_der, 'ASN.1 DER')
|
297
298
|
rescue StandardError => e
|
298
|
-
raise unless e.message
|
299
|
+
raise unless e.message.match?(/TAG.*not handled/)
|
299
300
|
end
|
300
301
|
str
|
301
302
|
end
|
data/lib/packetgen/header/tcp.rb
CHANGED
@@ -121,7 +121,7 @@ module PacketGen
|
|
121
121
|
# @!attribute options
|
122
122
|
# TCP options
|
123
123
|
# @return [Options]
|
124
|
-
define_field :options, TCP::Options
|
124
|
+
define_field :options, TCP::Options, builder: ->(h, t) { t.new(length_from: -> { h.data_offset > 5 ? (h.data_offset - 5) * 4 : 0 }) }
|
125
125
|
# @!attribute body
|
126
126
|
# @return [Types::String,Header::Base]
|
127
127
|
define_field :body, Types::String
|
@@ -189,25 +189,6 @@ module PacketGen
|
|
189
189
|
# @return [Boolean] 1-bit FIN flag
|
190
190
|
define_bit_fields_on :u16, :_, 7, :flag_ns, :flag_cwr, :flag_ece, :flag_urg,
|
191
191
|
:flag_ack, :flag_psh, :flag_rst, :flag_syn, :flag_fin
|
192
|
-
# Read a TCP header from a string
|
193
|
-
# @param [String] str binary string
|
194
|
-
# @return [self]
|
195
|
-
def read(str)
|
196
|
-
return self if str.nil?
|
197
|
-
|
198
|
-
force_binary str
|
199
|
-
self[:sport].read str[0, 2]
|
200
|
-
self[:dport].read str[2, 2]
|
201
|
-
self[:seqnum].read str[4, 4]
|
202
|
-
self[:acknum].read str[8, 4]
|
203
|
-
self[:u16].read str[12, 2]
|
204
|
-
self[:window].read str[14, 2]
|
205
|
-
self[:checksum].read str[16, 2]
|
206
|
-
self[:urg_pointer].read str[18, 2]
|
207
|
-
self[:options].read str[20, (self.data_offset - 5) * 4] if self.data_offset > 5
|
208
|
-
self[:body].read str[self.data_offset * 4..-1]
|
209
|
-
self
|
210
|
-
end
|
211
192
|
|
212
193
|
# Compute checksum and set +checksum+ field
|
213
194
|
# @return [Integer]
|
@@ -11,6 +11,8 @@ module PacketGen
|
|
11
11
|
# Base class to describe a TCP option
|
12
12
|
# @author Sylvain Daubert
|
13
13
|
class Option < Types::Fields
|
14
|
+
include Types::Fieldable
|
15
|
+
|
14
16
|
# EOL option value
|
15
17
|
EOL_KIND = 0
|
16
18
|
# NOP option value
|
@@ -91,15 +93,15 @@ module PacketGen
|
|
91
93
|
# Setter for value attribute
|
92
94
|
# @param[String,Integer]
|
93
95
|
# @return [String, Integer]
|
94
|
-
def value=(
|
96
|
+
def value=(val)
|
95
97
|
case self[:value]
|
96
98
|
when Types::Int
|
97
99
|
self.length = 2 + self[:value].sz
|
98
|
-
when String
|
99
|
-
self.length = 2 + Types::String.new.read(
|
100
|
+
when Types::String
|
101
|
+
self.length = 2 + Types::String.new.read(val).sz
|
100
102
|
end
|
101
|
-
self[:value].read
|
102
|
-
|
103
|
+
self[:value].read val
|
104
|
+
val
|
103
105
|
end
|
104
106
|
|
105
107
|
# Get binary string
|
@@ -112,7 +114,7 @@ module PacketGen
|
|
112
114
|
# Get option as a human readable string
|
113
115
|
# @return [String]
|
114
116
|
def to_human
|
115
|
-
str = self.
|
117
|
+
str = self.instance_of?(Option) ? +"unk-#{kind}" : self.class.to_s.sub(/.*::/, '')
|
116
118
|
str << ":#{self[:value].to_s.inspect}" if (length > 2) && !self[:value].to_s.empty?
|
117
119
|
str
|
118
120
|
end
|
data/lib/packetgen/inspect.rb
CHANGED
@@ -68,23 +68,7 @@ module PacketGen
|
|
68
68
|
# @return [String]
|
69
69
|
def self.inspect_attribute(attr, value, level=1)
|
70
70
|
type = value.class.to_s.sub(/.*::/, '')
|
71
|
-
|
72
|
-
when Types::Enum
|
73
|
-
enum_human_hex(value.to_human, value.to_i, value.sz * 2)
|
74
|
-
when Types::Int
|
75
|
-
int_dec_hex(value, value.sz * 2)
|
76
|
-
when Integer
|
77
|
-
int_dec_hex(value, value.sz * 2)
|
78
|
-
when String
|
79
|
-
value.to_s.inspect
|
80
|
-
else
|
81
|
-
if value.respond_to? :to_human
|
82
|
-
value.to_human
|
83
|
-
else
|
84
|
-
value.to_s.inspect
|
85
|
-
end
|
86
|
-
end
|
87
|
-
self.format(type, attr, val, level)
|
71
|
+
self.format(type, attr, value.format_inspect, level)
|
88
72
|
end
|
89
73
|
|
90
74
|
# Format a ASN.1 attribute for +#inspect+.
|