macker 0.1.0
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 +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +26 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/Rakefile +16 -0
- data/data/oui_1499079803.txt +141330 -0
- data/lib/macker.rb +394 -0
- data/lib/macker/address.rb +137 -0
- data/lib/macker/config.rb +32 -0
- data/lib/macker/version.rb +7 -0
- data/macker.gemspec +30 -0
- data/test/maker_test.rb +7 -0
- metadata +114 -0
data/lib/macker.rb
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'macker/version'
|
6
|
+
require 'macker/config'
|
7
|
+
require 'macker/address'
|
8
|
+
|
9
|
+
# Macker namespace
|
10
|
+
module Macker
|
11
|
+
# Invalid cache, file not found or cache empty
|
12
|
+
class InvalidCache < StandardError; end
|
13
|
+
# Invalid raw data, file not found or cache empty
|
14
|
+
class InvalidRawData < StandardError; end
|
15
|
+
# Invalid options, invalid options given to method
|
16
|
+
class InvalidOptions < StandardError; end
|
17
|
+
# Invalid OUI vendor, vendor not found in table
|
18
|
+
class NotFoundOuiVendor < StandardError; end
|
19
|
+
|
20
|
+
# Extend instance methods to class methods
|
21
|
+
extend self
|
22
|
+
|
23
|
+
# Proc timestamp accessor to set external timestamp
|
24
|
+
# @param value [Time] proc timestamp
|
25
|
+
# @return [Time] proc timestamp
|
26
|
+
attr_accessor :proc_timestamp
|
27
|
+
|
28
|
+
# Get the timestamp of vendor list in memory
|
29
|
+
# @return [Time] time object or nil
|
30
|
+
attr_reader :mem_timestamp
|
31
|
+
|
32
|
+
# Get or initialize Macker config.
|
33
|
+
# @return [OpenStruct] Macker configuration
|
34
|
+
def config
|
35
|
+
@config ||= Macker::Config.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set configuration of Macker in a block.
|
39
|
+
# @return [OpenStruct] Macker configuration
|
40
|
+
def configure
|
41
|
+
yield config if block_given?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Update all OUI tables
|
45
|
+
# @param straight [Boolean] true for straight, default is careful
|
46
|
+
# @return [Time] timestamp of the update
|
47
|
+
def update(straight = false)
|
48
|
+
@prefix_table = {}
|
49
|
+
@iso_code_table = {}
|
50
|
+
@vendor_table = {}
|
51
|
+
vendor_list(straight)
|
52
|
+
@mem_timestamp = if config.cache.is_a?(Proc)
|
53
|
+
proc_timestamp
|
54
|
+
else
|
55
|
+
file_timestamp
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Lookup for a vendor with given MAC address.
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# lookup('00:04:A9:6D:B8:AC')
|
63
|
+
# lookup(20022409388)
|
64
|
+
#
|
65
|
+
# @param mac [Address,Integer,String] MAC address
|
66
|
+
# @param opts [Hash] options for the method
|
67
|
+
# @return [Address] MAC address with vendor data
|
68
|
+
def lookup(mac, opts = {})
|
69
|
+
expire! if config.auto_expiration
|
70
|
+
data = prefix_table[Address.new(mac).prefix]
|
71
|
+
if data.nil?
|
72
|
+
opts[:raising] ? raise(NotFoundOuiVendor, "OUI not found for MAC: #{mac}") : (return nil)
|
73
|
+
end
|
74
|
+
Address.new(mac, data)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Lookup for a vendor with given MAC address.
|
78
|
+
# Raises an error if no vendor found.
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# lookup!('00:04:A9:6D:B8:AC')
|
82
|
+
# lookup!('80:47:FB:B2:9E:D6')
|
83
|
+
#
|
84
|
+
# @param mac [Address,Integer,String] MAC address
|
85
|
+
# @return [Address] MAC address with vendor data
|
86
|
+
def lookup!(mac)
|
87
|
+
lookup(mac, raising: true)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Generate a MAC address.
|
91
|
+
# - No options for random MAC.
|
92
|
+
# - Vendor option to get a valid OUI MAC.
|
93
|
+
# - Vendor Name option to get a random MAC from vendor.
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# generate
|
97
|
+
# generate(vendor: true)
|
98
|
+
# generate(vendor: 'IEEE Registration Authority')
|
99
|
+
#
|
100
|
+
# @param opts [Hash] options for the method
|
101
|
+
# @return [Address] MAC address with data
|
102
|
+
def generate(opts = {})
|
103
|
+
expire! if config.auto_expiration
|
104
|
+
return generate_by_iso_code(opts.delete(:iso_code), opts) if opts[:iso_code] && opts[:iso_code].length == 2
|
105
|
+
vendor = opts.delete(:vendor)
|
106
|
+
case vendor
|
107
|
+
when nil, false
|
108
|
+
Address.new(rand(2**48))
|
109
|
+
when true
|
110
|
+
generate_by_vendor(prefix_table[prefix_table.keys.shuffle.sample][:name], opts)
|
111
|
+
when String
|
112
|
+
generate_by_vendor(vendor, opts)
|
113
|
+
else
|
114
|
+
raise(InvalidOptions, "Incompatible option vendor for generate: #{vendor.class}")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Generate a MAC address.
|
119
|
+
# - No options for random MAC address.
|
120
|
+
# - Vendor option to get a valid OUI MAC address.
|
121
|
+
# - Vendor name option to get a random MAC address from vendor.
|
122
|
+
# Raises an error if an error occurs.
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# generate
|
126
|
+
# generate(vendor: true)
|
127
|
+
# generate(vendor: 'No vendor')
|
128
|
+
#
|
129
|
+
# @param opts [Hash] options for the method
|
130
|
+
# @return [Address] MAC address with vendor data
|
131
|
+
def generate!(opts = {})
|
132
|
+
generate(opts.merge(raising: true))
|
133
|
+
end
|
134
|
+
|
135
|
+
# Vendor table with all base16 MAC prefixes as keys
|
136
|
+
# @return [Hash] vendor prefixes table
|
137
|
+
def prefix_table
|
138
|
+
update unless @prefix_table
|
139
|
+
@prefix_table
|
140
|
+
end
|
141
|
+
|
142
|
+
# Vendor table with all country iso codes as keys
|
143
|
+
# @return [Hash] vendor iso codes table
|
144
|
+
def iso_code_table
|
145
|
+
update unless @iso_code_table
|
146
|
+
@iso_code_table
|
147
|
+
end
|
148
|
+
|
149
|
+
# Vendor table with all country vendor names as keys
|
150
|
+
# @return [Hash] vendor names table
|
151
|
+
def vendor_table
|
152
|
+
update unless @vendor_table
|
153
|
+
@vendor_table
|
154
|
+
end
|
155
|
+
|
156
|
+
# Fetch new vendor list if cached list is expired or stale
|
157
|
+
# @return [Boolean] true if vendor list is expired and updated from remote
|
158
|
+
def expire!
|
159
|
+
if expired?
|
160
|
+
update(true)
|
161
|
+
true
|
162
|
+
elsif stale?
|
163
|
+
update
|
164
|
+
true
|
165
|
+
else
|
166
|
+
false
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Check if vendor list is expired
|
171
|
+
# @return [Boolean] true if vendor list is expired
|
172
|
+
def expired?
|
173
|
+
Time.now > vendors_expiration
|
174
|
+
end
|
175
|
+
|
176
|
+
# Check if vendor list is stale
|
177
|
+
# Stale is true if vendor list is updated straight by another thread.
|
178
|
+
# The actual thread has always old vendor list in memory store.
|
179
|
+
# @return [Boolean] true if vendor list is stale
|
180
|
+
def stale?
|
181
|
+
if config.cache.is_a?(Proc)
|
182
|
+
proc_timestamp != mem_timestamp
|
183
|
+
else
|
184
|
+
file_timestamp != mem_timestamp
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Get vendor list expiration time based on ttl
|
189
|
+
# @return [Time] vendor list expiration time
|
190
|
+
def vendors_expiration
|
191
|
+
if config.cache.is_a?(Proc)
|
192
|
+
proc_timestamp + config.ttl_in_seconds
|
193
|
+
else
|
194
|
+
file_timestamp + config.ttl_in_seconds
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
protected
|
199
|
+
|
200
|
+
# Generate a MAC address by vendor.
|
201
|
+
#
|
202
|
+
# @param vendor [String] name of vendor
|
203
|
+
# @param opts [Hash] options for the method
|
204
|
+
# @return [Address] MAC address with vendor data
|
205
|
+
def generate_by_vendor(vendor, opts = {})
|
206
|
+
ouis = vendor_table[vendor]
|
207
|
+
if ouis.nil? || ouis.empty?
|
208
|
+
opts[:raising] ? raise(NotFoundOuiVendor, "OUI not found for vendor: #{vendor}") : (return nil)
|
209
|
+
end
|
210
|
+
oui = ouis[rand(ouis.size)]
|
211
|
+
m1 = Address.new(oui[:prefix]).to_i
|
212
|
+
m2 = rand(2**24)
|
213
|
+
mac = m1 + m2
|
214
|
+
Address.new(mac,
|
215
|
+
name: vendor,
|
216
|
+
address: oui[:address],
|
217
|
+
iso_code: oui[:iso_code])
|
218
|
+
end
|
219
|
+
|
220
|
+
# Generate a MAC address by iso code.
|
221
|
+
#
|
222
|
+
# @param iso_code [String] iso code
|
223
|
+
# @param opts [Hash] options for the method
|
224
|
+
# @return [Address] MAC address with vendor data
|
225
|
+
def generate_by_iso_code(iso_code, opts = {})
|
226
|
+
ouis = iso_code_table[iso_code]
|
227
|
+
if ouis.nil? || ouis.empty?
|
228
|
+
opts[:raising] ? raise(NotFoundOuiVendor, "OUI not found for iso code #{iso_code}") : (return nil)
|
229
|
+
end
|
230
|
+
oui = ouis[rand(ouis.size)]
|
231
|
+
m1 = Address.new(oui[:prefix]).to_i
|
232
|
+
m2 = rand(2**24)
|
233
|
+
mac = m1 + m2
|
234
|
+
Address.new(mac,
|
235
|
+
name: oui[:name],
|
236
|
+
address: oui[:address],
|
237
|
+
iso_code: iso_code)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Get vendor list with different strategies.
|
241
|
+
# Parse and read in the content.
|
242
|
+
#
|
243
|
+
# @example
|
244
|
+
# vendor_list(true)
|
245
|
+
# vendor_list
|
246
|
+
#
|
247
|
+
# @param straight [Boolean] true for straight, default is careful
|
248
|
+
# @return [Hash] vendor list with all base16 MAC prefixes as keys
|
249
|
+
def vendor_list(straight = false)
|
250
|
+
raw_vendor_list = if straight
|
251
|
+
raw_vendor_list_straight
|
252
|
+
else
|
253
|
+
raw_vendor_list_careful
|
254
|
+
end
|
255
|
+
vendor_list = raw_vendor_list.gsub(/\r\n/, "\n").gsub(/\t+/, "\t").split(/\n\n/)
|
256
|
+
vendor_list[1..-1].each do |vendor|
|
257
|
+
base16_fields = vendor.strip.split("\n")[1].split("\t")
|
258
|
+
mac_prefix = Address.new(base16_fields[0].strip[0..5]).prefix
|
259
|
+
address = vendor.strip.delete("\t").split("\n")
|
260
|
+
next unless @prefix_table[mac_prefix].nil?
|
261
|
+
@prefix_table[mac_prefix] = { name: base16_fields[-1]
|
262
|
+
.strip
|
263
|
+
.gsub(/\s+/, ' ')
|
264
|
+
.split(/ |\_|\-/)
|
265
|
+
.map(&:capitalize)
|
266
|
+
.join(' '),
|
267
|
+
address: address[2..-1].map { |a| a
|
268
|
+
.strip
|
269
|
+
.gsub(/\s+/, ' ')
|
270
|
+
.gsub(/,(?![\s])/, ', ')
|
271
|
+
.gsub(/\,+$/, '')
|
272
|
+
.split(/ |\_|\-/)
|
273
|
+
.map(&:capitalize)
|
274
|
+
.join(' ')
|
275
|
+
},
|
276
|
+
iso_code: address[-1].strip.upcase
|
277
|
+
}
|
278
|
+
end
|
279
|
+
@iso_code_table = @prefix_table.each_with_object({}) { |(key, value), out| (out[value[:iso_code]] ||= []) << value.merge(prefix: key) }
|
280
|
+
@vendor_table = @prefix_table.each_with_object({}) { |(key, value), out| (out[value[:name]] ||= []) << value.merge(prefix: key) }
|
281
|
+
@prefix_table
|
282
|
+
end
|
283
|
+
|
284
|
+
# Get raw vendor list from cache and then from url
|
285
|
+
# @param rescue_straight [Boolean] true for rescue straight, default true
|
286
|
+
# @return [String] text content
|
287
|
+
def raw_vendor_list_careful(rescue_straight = true)
|
288
|
+
res = read_from_cache
|
289
|
+
raise if res.to_s.empty?
|
290
|
+
res
|
291
|
+
rescue
|
292
|
+
rescue_straight ? raw_vendor_list_straight : ''
|
293
|
+
end
|
294
|
+
|
295
|
+
# Get raw vendor list from url
|
296
|
+
# @return [String] text content
|
297
|
+
def raw_vendor_list_straight
|
298
|
+
res = read_from_url
|
299
|
+
raise if res.to_s.empty?
|
300
|
+
res
|
301
|
+
rescue
|
302
|
+
raw_vendor_list_careful(false)
|
303
|
+
end
|
304
|
+
|
305
|
+
# Store the provided text data by calling the proc method provided
|
306
|
+
# for the cache, or write to the cache file.
|
307
|
+
#
|
308
|
+
# @example
|
309
|
+
# store_in_cache("E0-43-DB (hex) Shenzhen ViewAt Technology Co.,Ltd.
|
310
|
+
# E043DB (base 16) Shenzhen ViewAt Technology Co.,Ltd.
|
311
|
+
# 9A,Microprofit,6th Gaoxin South...
|
312
|
+
# shenzhen guangdong 518057
|
313
|
+
# CN
|
314
|
+
# ")
|
315
|
+
#
|
316
|
+
# @param text [String] text content
|
317
|
+
# @return [Integer] normally 0
|
318
|
+
def store_in_cache(text)
|
319
|
+
if config.cache.is_a?(Proc)
|
320
|
+
config.cache.call(text)
|
321
|
+
elsif config.cache.is_a?(String) || config.cache.is_a?(Pathname)
|
322
|
+
write_to_file(text)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Writes content to file cache
|
327
|
+
# @param text [String] text content
|
328
|
+
# @return [Integer] normally 0
|
329
|
+
def write_to_file(text)
|
330
|
+
open(file_path, 'w') do |f|
|
331
|
+
f.write(text)
|
332
|
+
end
|
333
|
+
file_update
|
334
|
+
rescue Errno::ENOENT
|
335
|
+
raise InvalidCache
|
336
|
+
end
|
337
|
+
|
338
|
+
# Read from cache when exist
|
339
|
+
# @return [Proc,String] text content
|
340
|
+
def read_from_cache
|
341
|
+
if config.cache.is_a?(Proc)
|
342
|
+
config.cache.call(nil)
|
343
|
+
elsif (config.cache.is_a?(String) || config.cache.is_a?(Pathname)) &&
|
344
|
+
File.exist?(file_path)
|
345
|
+
open(file_path).read
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Get remote content and store in cache
|
350
|
+
# @return [String] text content
|
351
|
+
def read_from_url
|
352
|
+
text = open_url
|
353
|
+
store_in_cache(text) if text && config.cache
|
354
|
+
text
|
355
|
+
end
|
356
|
+
|
357
|
+
# Opens an URL and reads the content
|
358
|
+
# @return [String] text content
|
359
|
+
def open_url
|
360
|
+
opts = [config.oui_full_url]
|
361
|
+
opts << { 'User-Agent' => config.user_agent } if config.user_agent
|
362
|
+
open(*opts).read
|
363
|
+
rescue OpenURI::HTTPError
|
364
|
+
''
|
365
|
+
end
|
366
|
+
|
367
|
+
# Get file path with timestamp
|
368
|
+
# @return [String] file path
|
369
|
+
def file_path
|
370
|
+
Dir.glob(config.cache).first || File.join(File.dirname(config.cache),
|
371
|
+
File.basename(config.cache).gsub(/_.+\.txt/, '_0.txt'))
|
372
|
+
end
|
373
|
+
|
374
|
+
# Get file name with timestamp
|
375
|
+
# @return [String] file name
|
376
|
+
def file_name
|
377
|
+
File.basename(file_path)
|
378
|
+
end
|
379
|
+
|
380
|
+
# Get file timestamp
|
381
|
+
# @return [Time] file timestamp
|
382
|
+
def file_timestamp
|
383
|
+
timestamp = file_name.scan(/\d+/).first
|
384
|
+
timestamp ? Time.at(timestamp.to_i) : Time.at(0)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Update file name timestamp
|
388
|
+
# @return [Integer] normally 0
|
389
|
+
def file_update
|
390
|
+
File.rename(file_path,
|
391
|
+
File.join(File.dirname(file_path),
|
392
|
+
File.basename(file_path).gsub(/_\d+\.txt/, "_#{Time.now.to_i}.txt")))
|
393
|
+
end
|
394
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# Macker namespace
|
4
|
+
module Macker
|
5
|
+
# Invalid address, mac address format not valid
|
6
|
+
class InvalidAddress < StandardError; end
|
7
|
+
# MAC address class
|
8
|
+
class Address
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
# Get the value of name, address or iso code
|
12
|
+
# @return [String] content of the value
|
13
|
+
attr_reader :name, :address, :iso_code
|
14
|
+
|
15
|
+
# Initialize Address object
|
16
|
+
# @param mac [Address,Integer,String] a MAC address
|
17
|
+
# @param opts [Hash] options for the method
|
18
|
+
# @return [Address] the initialized object
|
19
|
+
def initialize(mac, opts = {})
|
20
|
+
case mac
|
21
|
+
when Address
|
22
|
+
@val = mac.to_i
|
23
|
+
@name = mac.name
|
24
|
+
@address = mac.address
|
25
|
+
@iso_code = mac.iso_code
|
26
|
+
when Integer
|
27
|
+
@val = mac
|
28
|
+
when String
|
29
|
+
@val = cleanup(mac).to_i(16)
|
30
|
+
else
|
31
|
+
raise(InvalidAddress, "Incompatible type for address initialization: #{mac.class}")
|
32
|
+
end
|
33
|
+
raise(InvalidAddress, "Invalid MAC address: #{self.to_s}") unless valid?
|
34
|
+
@name ||= opts.fetch(:name, nil)
|
35
|
+
@address ||= opts.fetch(:address, nil)
|
36
|
+
@iso_code ||= opts.fetch(:iso_code, nil)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Format MAC address to integer
|
40
|
+
# @return [Integer] integer MAC address
|
41
|
+
def to_i
|
42
|
+
@val
|
43
|
+
end
|
44
|
+
|
45
|
+
# Format MAC address to string
|
46
|
+
# @param sep [String] separator, default is ':'
|
47
|
+
# @return [String] formatted MAC address
|
48
|
+
def to_s(sep = ':')
|
49
|
+
@val.to_s(16)
|
50
|
+
.rjust(12, '0')
|
51
|
+
.insert(10, sep)
|
52
|
+
.insert(8, sep)
|
53
|
+
.insert(6, sep)
|
54
|
+
.insert(4, sep)
|
55
|
+
.insert(2, sep)
|
56
|
+
.upcase
|
57
|
+
end
|
58
|
+
|
59
|
+
# Compare two MAC addresses
|
60
|
+
# @param other [Address] MAC address object
|
61
|
+
# @return [Boolean] true if the same, else false
|
62
|
+
def <=>(other)
|
63
|
+
@val <=> other.to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if MAC address is an OUI valid address
|
67
|
+
# @return [Boolean] true if valid, else false
|
68
|
+
def oui?
|
69
|
+
!@name.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if MAC address is a valid address
|
73
|
+
# @return [Boolean] true if valid, else false
|
74
|
+
def valid?
|
75
|
+
@val.between?(0, 2**48 - 1)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Check if MAC address is a broadcast address
|
79
|
+
# @return [Boolean] true if broadcast, else false
|
80
|
+
def broadcast?
|
81
|
+
@val == 2**48 - 1
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check if MAC address is an unicast address
|
85
|
+
# @return [Boolean] true if unicast, else false
|
86
|
+
def unicast?
|
87
|
+
!multicast?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Check if MAC address is a multicast address
|
91
|
+
# @return [Boolean] true if multicast, else false
|
92
|
+
def multicast?
|
93
|
+
mask = 1 << (5 * 8)
|
94
|
+
(mask & @val) != 0
|
95
|
+
end
|
96
|
+
|
97
|
+
# Check if MAC address is a global uniq address
|
98
|
+
# @return [Boolean] true if uniq, else false
|
99
|
+
def global_uniq?
|
100
|
+
!local_admin?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Check if MAC address is a local address
|
104
|
+
# @return [Boolean] true if local, else false
|
105
|
+
def local_admin?
|
106
|
+
mask = 2 << (5 * 8)
|
107
|
+
(mask & @val) != 0
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get next MAC address from actual address
|
111
|
+
# @return [Adress] next MAC address
|
112
|
+
def next
|
113
|
+
Address.new((@val + 1) % 2**48)
|
114
|
+
end
|
115
|
+
alias succ next
|
116
|
+
|
117
|
+
# Get the prefix base16 MAC address
|
118
|
+
# @return [Adress] MAC prefix
|
119
|
+
def prefix
|
120
|
+
to_s('')[0..5]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Get the full vendor address
|
124
|
+
# @return [String] full vendor address string
|
125
|
+
def full_address
|
126
|
+
address.join(', ')
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Clean up a MAC string from special characters
|
132
|
+
# @return [String] cleaned MAC address
|
133
|
+
def cleanup(mac)
|
134
|
+
mac.strip.upcase.gsub(/^0[xX]/, '').gsub(/[^0-9A-F]/, '').ljust(12, '0')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|