dns-zone2 0.3.2
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/Gemfile +2 -0
- data/HISTORY.md +45 -0
- data/README.md +151 -0
- data/Rakefile +15 -0
- data/dns-zone2.gemspec +41 -0
- data/lib/dns/zone.rb +207 -0
- data/lib/dns/zone/rr.rb +87 -0
- data/lib/dns/zone/rr/a.rb +21 -0
- data/lib/dns/zone/rr/aaaa.rb +5 -0
- data/lib/dns/zone/rr/cdnskey.rb +5 -0
- data/lib/dns/zone/rr/cds.rb +5 -0
- data/lib/dns/zone/rr/cname.rb +21 -0
- data/lib/dns/zone/rr/dlv.rb +5 -0
- data/lib/dns/zone/rr/dnskey.rb +38 -0
- data/lib/dns/zone/rr/ds.rb +38 -0
- data/lib/dns/zone/rr/hinfo.rb +31 -0
- data/lib/dns/zone/rr/mx.rb +33 -0
- data/lib/dns/zone/rr/naptr.rb +44 -0
- data/lib/dns/zone/rr/ns.rb +21 -0
- data/lib/dns/zone/rr/nsec.rb +32 -0
- data/lib/dns/zone/rr/nsec3.rb +45 -0
- data/lib/dns/zone/rr/nsec3param.rb +38 -0
- data/lib/dns/zone/rr/ptr.rb +21 -0
- data/lib/dns/zone/rr/record.rb +88 -0
- data/lib/dns/zone/rr/rrsig.rb +54 -0
- data/lib/dns/zone/rr/soa.rb +51 -0
- data/lib/dns/zone/rr/spf.rb +5 -0
- data/lib/dns/zone/rr/srv.rb +38 -0
- data/lib/dns/zone/rr/sshfp.rb +35 -0
- data/lib/dns/zone/rr/txt.rb +24 -0
- data/lib/dns/zone/test_case.rb +27 -0
- data/lib/dns/zone/version.rb +6 -0
- data/test/rr/a_test.rb +37 -0
- data/test/rr/aaaa_test.rb +27 -0
- data/test/rr/cdnskey_test.rb +31 -0
- data/test/rr/cds_test.rb +28 -0
- data/test/rr/cname_test.rb +19 -0
- data/test/rr/dlv_test.rb +28 -0
- data/test/rr/dnskey_test.rb +31 -0
- data/test/rr/ds_test.rb +28 -0
- data/test/rr/hinfo_test.rb +44 -0
- data/test/rr/mx_test.rb +26 -0
- data/test/rr/naptr_test.rb +60 -0
- data/test/rr/ns_test.rb +18 -0
- data/test/rr/nsec3_test.rb +33 -0
- data/test/rr/nsec3param_test.rb +29 -0
- data/test/rr/nsec_test.rb +24 -0
- data/test/rr/ptr_test.rb +19 -0
- data/test/rr/record_test.rb +37 -0
- data/test/rr/rrsig_test.rb +40 -0
- data/test/rr/soa_test.rb +34 -0
- data/test/rr/spf_test.rb +20 -0
- data/test/rr/srv_test.rb +24 -0
- data/test/rr/sshfp_test.rb +24 -0
- data/test/rr/txt_test.rb +44 -0
- data/test/rr_test.rb +50 -0
- data/test/version_test.rb +9 -0
- data/test/zone_test.rb +273 -0
- metadata +217 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# `NSEC3PARAM` resource record.
|
2
|
+
#
|
3
|
+
# RFC 5155
|
4
|
+
class DNS::Zone::RR::NSEC3PARAM < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_NSEC3PARAM_RDATA = %r{
|
7
|
+
(?<algorithm>\d+)\s*
|
8
|
+
(?<flags>\d+)\s*
|
9
|
+
(?<iterations>\d+)\s*
|
10
|
+
(?<salt>\S+)\s*
|
11
|
+
}mx
|
12
|
+
|
13
|
+
attr_accessor :algorithm, :flags, :iterations, :salt
|
14
|
+
|
15
|
+
def dump
|
16
|
+
parts = general_prefix
|
17
|
+
parts << algorithm
|
18
|
+
parts << flags
|
19
|
+
parts << iterations
|
20
|
+
parts << salt
|
21
|
+
parts.join(' ')
|
22
|
+
end
|
23
|
+
|
24
|
+
def load(string, options = {})
|
25
|
+
rdata = load_general_and_get_rdata(string, options)
|
26
|
+
return nil unless rdata
|
27
|
+
|
28
|
+
captures = rdata.match(REGEX_NSEC3PARAM_RDATA)
|
29
|
+
return nil unless captures
|
30
|
+
|
31
|
+
@algorithm = captures[:algorithm].to_i
|
32
|
+
@flags = captures[:flags].to_i
|
33
|
+
@iterations = captures[:iterations].to_i
|
34
|
+
@salt = captures[:salt]
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# `PTR` resource record.
|
2
|
+
#
|
3
|
+
# RFC 1035
|
4
|
+
class DNS::Zone::RR::PTR < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
attr_accessor :name
|
7
|
+
|
8
|
+
def dump
|
9
|
+
parts = general_prefix
|
10
|
+
parts << @name
|
11
|
+
parts.join(' ')
|
12
|
+
end
|
13
|
+
|
14
|
+
def load(string, options = {})
|
15
|
+
rdata = load_general_and_get_rdata(string, options)
|
16
|
+
return nil unless rdata
|
17
|
+
@name = rdata
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# Parent class of all RR types, common resource record code lives here.
|
2
|
+
# Is responsible for building a Ruby object given a RR string.
|
3
|
+
#
|
4
|
+
# @abstract Each RR TYPE should subclass and override: {#load} and #{dump}
|
5
|
+
class DNS::Zone::RR::Record
|
6
|
+
|
7
|
+
attr_accessor :label, :ttl
|
8
|
+
attr_reader :klass
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@label = '@'
|
12
|
+
@klass = 'IN'
|
13
|
+
end
|
14
|
+
|
15
|
+
# FIXME: should we just: `def type; 'SOA'; end` rather then do the class name convension?
|
16
|
+
#
|
17
|
+
# Figures out TYPE of RR using class name.
|
18
|
+
# This means the class name _must_ match the RR ASCII TYPE.
|
19
|
+
#
|
20
|
+
# When called directly on the parent class (that you should never do), it will
|
21
|
+
# return the string as `<type>`, for use with internal tests.
|
22
|
+
#
|
23
|
+
# @return [String] the RR type
|
24
|
+
def type
|
25
|
+
name = self.class.name.split('::').last
|
26
|
+
return '<type>' if name == 'Record'
|
27
|
+
name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns 'general' prefix (in parts) that come before the RDATA.
|
31
|
+
# Used by all RR types, generates: `[<label>] [<ttl>] [<class>] <type>`
|
32
|
+
#
|
33
|
+
# @return [Array<String>] rr prefix parts
|
34
|
+
def general_prefix
|
35
|
+
parts = []
|
36
|
+
parts << label
|
37
|
+
parts << ttl if ttl
|
38
|
+
parts << 'IN'
|
39
|
+
parts << type
|
40
|
+
parts
|
41
|
+
end
|
42
|
+
|
43
|
+
# Build RR zone file output.
|
44
|
+
#
|
45
|
+
# @return [String] RR zone file output
|
46
|
+
def dump
|
47
|
+
general_prefix.join(' ')
|
48
|
+
end
|
49
|
+
|
50
|
+
# @abstract Override to update instance with RR type spesific data.
|
51
|
+
# @param string [String] RR ASCII string data
|
52
|
+
# @param options [Hash] additional data required to correctly parse a 'whole' zone
|
53
|
+
# @option options [String] :last_label The last label used by the previous RR
|
54
|
+
# @return [Object]
|
55
|
+
def load(string, options = {})
|
56
|
+
raise NotImplementedError, "#load method must be implemented by subclass (#{self.class})"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Load 'general' RR data/params and return the remaining RDATA for further parsing.
|
60
|
+
#
|
61
|
+
# @param string [String] RR ASCII string data
|
62
|
+
# @param options [Hash] additional data required to correctly parse a 'whole' zone
|
63
|
+
# @return [String] remaining RDATA
|
64
|
+
def load_general_and_get_rdata(string, options = {})
|
65
|
+
# strip comments, unless its escaped.
|
66
|
+
# skip semicolons within "quote segments" (TXT records)
|
67
|
+
string.gsub!(/((?<!\\);)(?=(?:[^"]|"[^"]*")*$).*/o, "")
|
68
|
+
|
69
|
+
captures = string.match(DNS::Zone::RR::REGEX_RR)
|
70
|
+
return nil unless captures
|
71
|
+
|
72
|
+
if [' ', nil].include?(captures[:label])
|
73
|
+
@label = options[:last_label]
|
74
|
+
else
|
75
|
+
@label = captures[:label]
|
76
|
+
end
|
77
|
+
|
78
|
+
# unroll records nested under other origins
|
79
|
+
unrolled_origin = options[:last_origin].sub(options[:origin], '').chomp('.') if options[:last_origin]
|
80
|
+
if unrolled_origin && !unrolled_origin.empty?
|
81
|
+
@label = @label == '@' ? unrolled_origin : "#{@label}.#{unrolled_origin}"
|
82
|
+
end
|
83
|
+
|
84
|
+
@ttl = captures[:ttl]
|
85
|
+
captures[:rdata]
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# `RRSIG` resource record.
|
2
|
+
#
|
3
|
+
# RFC 4034
|
4
|
+
class DNS::Zone::RR::RRSIG < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_RRSIG_RDATA = %r{
|
7
|
+
(?<type_covered>\S+)\s*
|
8
|
+
(?<algorithm>\d+)\s*
|
9
|
+
(?<labels>\d+)\s*
|
10
|
+
(?<original_ttl>#{DNS::Zone::RR::REGEX_TTL})\s*
|
11
|
+
(?<signature_expiration>\d+)\s*
|
12
|
+
(?<signature_inception>\d+)\s*
|
13
|
+
(?<key_tag>\d+)\s*
|
14
|
+
(?<signer>#{DNS::Zone::RR::REGEX_DOMAINNAME})\s*
|
15
|
+
(?<signature>#{DNS::Zone::RR::REGEX_CHARACTER_STRING})\s*
|
16
|
+
}mx
|
17
|
+
|
18
|
+
attr_accessor :type_covered, :algorithm, :labels, :original_ttl, :signature_expiration,
|
19
|
+
:signature_inception, :key_tag, :signer, :signature
|
20
|
+
|
21
|
+
def dump
|
22
|
+
parts = general_prefix
|
23
|
+
parts << type_covered
|
24
|
+
parts << algorithm
|
25
|
+
parts << labels
|
26
|
+
parts << original_ttl
|
27
|
+
parts << signature_expiration
|
28
|
+
parts << signature_inception
|
29
|
+
parts << key_tag
|
30
|
+
parts << signer
|
31
|
+
parts << signature
|
32
|
+
parts.join(' ')
|
33
|
+
end
|
34
|
+
|
35
|
+
def load(string, options = {})
|
36
|
+
rdata = load_general_and_get_rdata(string, options)
|
37
|
+
return nil unless rdata
|
38
|
+
|
39
|
+
captures = rdata.match(REGEX_RRSIG_RDATA)
|
40
|
+
return nil unless captures
|
41
|
+
|
42
|
+
@type_covered = captures[:type_covered]
|
43
|
+
@algorithm = captures[:algorithm].to_i
|
44
|
+
@labels = captures[:labels].to_i
|
45
|
+
@original_ttl = captures[:original_ttl].to_i
|
46
|
+
@signature_expiration = captures[:signature_expiration].to_i
|
47
|
+
@signature_inception = captures[:signature_inception].to_i
|
48
|
+
@key_tag = captures[:key_tag].to_i
|
49
|
+
@signer = captures[:signer]
|
50
|
+
@signature = captures[:signature]
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# `SRV` resource record.
|
2
|
+
#
|
3
|
+
# RFC 1035
|
4
|
+
class DNS::Zone::RR::SOA < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_SOA_RDATA = %r{
|
7
|
+
(?<nameserver>#{DNS::Zone::RR::REGEX_DOMAINNAME})\s* # get nameserver domainname
|
8
|
+
(?<email>#{DNS::Zone::RR::REGEX_DOMAINNAME})\s* # get mailbox domainname
|
9
|
+
(?<serial>\d+)\s*
|
10
|
+
(?<refresh_ttl>#{DNS::Zone::RR::REGEX_TTL})\s*
|
11
|
+
(?<retry_ttl>#{DNS::Zone::RR::REGEX_TTL})\s*
|
12
|
+
(?<expiry_ttl>#{DNS::Zone::RR::REGEX_TTL})\s*
|
13
|
+
(?<minimum_ttl>#{DNS::Zone::RR::REGEX_TTL})\s*
|
14
|
+
}mx
|
15
|
+
|
16
|
+
attr_accessor :nameserver, :email, :serial, :refresh_ttl, :retry_ttl, :expiry_ttl, :minimum_ttl
|
17
|
+
|
18
|
+
def dump
|
19
|
+
parts = general_prefix
|
20
|
+
parts << nameserver
|
21
|
+
parts << email
|
22
|
+
|
23
|
+
parts << '('
|
24
|
+
parts << serial
|
25
|
+
parts << refresh_ttl
|
26
|
+
parts << retry_ttl
|
27
|
+
parts << expiry_ttl
|
28
|
+
parts << minimum_ttl
|
29
|
+
parts << ')'
|
30
|
+
parts.join(' ')
|
31
|
+
end
|
32
|
+
|
33
|
+
def load(string, options = {})
|
34
|
+
rdata = load_general_and_get_rdata(string, options)
|
35
|
+
return nil unless rdata
|
36
|
+
|
37
|
+
captures = rdata.match(REGEX_SOA_RDATA)
|
38
|
+
return nil unless captures
|
39
|
+
|
40
|
+
@nameserver = captures[:nameserver]
|
41
|
+
@email = captures[:email]
|
42
|
+
@serial = captures[:serial].to_i
|
43
|
+
@refresh_ttl = captures[:refresh_ttl]
|
44
|
+
@retry_ttl = captures[:retry_ttl]
|
45
|
+
@expiry_ttl = captures[:expiry_ttl]
|
46
|
+
@minimum_ttl = captures[:minimum_ttl]
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# `SRV` resource record.
|
2
|
+
#
|
3
|
+
# RFC 2782
|
4
|
+
class DNS::Zone::RR::SRV < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_SRV_RDATA = %r{
|
7
|
+
(?<priority>\d+)\s*
|
8
|
+
(?<weight>\d+)\s*
|
9
|
+
(?<port>\d+)\s*
|
10
|
+
(?<target>#{DNS::Zone::RR::REGEX_DOMAINNAME})\s*
|
11
|
+
}mx
|
12
|
+
|
13
|
+
attr_accessor :priority, :weight, :port, :target
|
14
|
+
|
15
|
+
def dump
|
16
|
+
parts = general_prefix
|
17
|
+
parts << priority
|
18
|
+
parts << weight
|
19
|
+
parts << port
|
20
|
+
parts << target
|
21
|
+
parts.join(' ')
|
22
|
+
end
|
23
|
+
|
24
|
+
def load(string, options = {})
|
25
|
+
rdata = load_general_and_get_rdata(string, options)
|
26
|
+
return nil unless rdata
|
27
|
+
|
28
|
+
captures = rdata.match(REGEX_SRV_RDATA)
|
29
|
+
return nil unless captures
|
30
|
+
|
31
|
+
@priority = captures[:priority].to_i
|
32
|
+
@weight = captures[:weight].to_i
|
33
|
+
@port = captures[:port].to_i
|
34
|
+
@target = captures[:target]
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# `SSHFP` resource record.
|
2
|
+
#
|
3
|
+
# RFC 4255
|
4
|
+
class DNS::Zone::RR::SSHFP < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_SSHFP_RDATA = %r{
|
7
|
+
(?<algorithm_number>\d+)\s*
|
8
|
+
(?<fingerprint_type>\d+)\s*
|
9
|
+
(?<fingerprint>#{DNS::Zone::RR::REGEX_STRING})\s*
|
10
|
+
}mx
|
11
|
+
|
12
|
+
attr_accessor :algorithm_number, :fingerprint_type, :fingerprint
|
13
|
+
|
14
|
+
def dump
|
15
|
+
parts = general_prefix
|
16
|
+
parts << algorithm_number
|
17
|
+
parts << fingerprint_type
|
18
|
+
parts << fingerprint
|
19
|
+
parts.join(' ')
|
20
|
+
end
|
21
|
+
|
22
|
+
def load(string, options = {})
|
23
|
+
rdata = load_general_and_get_rdata(string, options)
|
24
|
+
return nil unless rdata
|
25
|
+
|
26
|
+
captures = rdata.match(REGEX_SSHFP_RDATA)
|
27
|
+
return nil unless captures
|
28
|
+
|
29
|
+
@algorithm_number = captures[:algorithm_number].to_i
|
30
|
+
@fingerprint_type = captures[:fingerprint_type].to_i
|
31
|
+
@fingerprint = captures[:fingerprint]
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# `A` resource record.
|
2
|
+
#
|
3
|
+
# RFC 1035
|
4
|
+
class DNS::Zone::RR::TXT < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
attr_accessor :text
|
7
|
+
|
8
|
+
def dump
|
9
|
+
parts = general_prefix
|
10
|
+
parts << text
|
11
|
+
parts.join(' ')
|
12
|
+
end
|
13
|
+
|
14
|
+
def load(string, options = {})
|
15
|
+
rdata = load_general_and_get_rdata(string, options)
|
16
|
+
return nil unless rdata
|
17
|
+
|
18
|
+
# extract text from within quotes; allow multiple quoted strings; ignore escaped quotes
|
19
|
+
# @text = rdata.scan(/"#{DNS::Zone::RR::REGEX_STRING}"/).flatten.map { |w| %Q{"#{w}"} }.join
|
20
|
+
@text = rdata.scan(/"#{DNS::Zone::RR::REGEX_STRING}"/).flat_map { |w| %Q{"#{w&.first}"} }.join(' ')
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#
|
2
|
+
# test_case.rb - A file used to setup the testing enviroment for the library.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
|
7
|
+
# --- code coverage on MRI 1.9 ruby only, but disabled by default --------------
|
8
|
+
if RUBY_VERSION >= '1.9' && RUBY_ENGINE == 'ruby' && ENV['COVERAGE']
|
9
|
+
require 'simplecov'
|
10
|
+
#SimpleCov.command_name 'test:unit'
|
11
|
+
SimpleCov.start do
|
12
|
+
# code coverage groups.
|
13
|
+
add_filter 'test/'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# --- load our dependencies using bundler --------------------------------------
|
18
|
+
require 'bundler/setup'
|
19
|
+
require 'minitest/autorun'
|
20
|
+
require 'minitest/pride'
|
21
|
+
|
22
|
+
# --- Load lib to test ---------------------------------------------------------
|
23
|
+
require 'dns/zone'
|
24
|
+
|
25
|
+
# --- Extend DNS::Zone::TestCase -------------------------------------------------
|
26
|
+
class DNS::Zone::TestCase < Minitest::Test
|
27
|
+
end
|
data/test/rr/a_test.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'dns/zone/test_case'
|
2
|
+
|
3
|
+
class RR_A_Test < DNS::Zone::TestCase
|
4
|
+
|
5
|
+
def test_build_rr__a
|
6
|
+
rr = DNS::Zone::RR::A.new
|
7
|
+
|
8
|
+
# ensure we can set address parameter
|
9
|
+
rr.address = '10.0.1.1'
|
10
|
+
assert_equal 'A', rr.type
|
11
|
+
assert_equal '@ IN A 10.0.1.1', rr.dump
|
12
|
+
rr.address = '10.0.2.2'
|
13
|
+
assert_equal '@ IN A 10.0.2.2', rr.dump
|
14
|
+
|
15
|
+
# with a label set
|
16
|
+
rr.label = 'labelname'
|
17
|
+
assert_equal 'labelname IN A 10.0.2.2', rr.dump
|
18
|
+
|
19
|
+
# with a ttl
|
20
|
+
rr.ttl = '4w'
|
21
|
+
assert_equal 'labelname 4w IN A 10.0.2.2', rr.dump
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_load_rr__a
|
25
|
+
rr = DNS::Zone::RR::A.new.load('@ IN A 127.0.0.1')
|
26
|
+
assert_equal '@', rr.label
|
27
|
+
assert_equal 'A', rr.type
|
28
|
+
assert_equal '127.0.0.1', rr.address
|
29
|
+
|
30
|
+
rr = DNS::Zone::RR::A.new.load('www IN A 127.0.0.1')
|
31
|
+
assert_equal 'www', rr.label
|
32
|
+
assert_equal 'A', rr.type
|
33
|
+
assert_equal 'IN', rr.klass
|
34
|
+
assert_equal '127.0.0.1', rr.address
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'dns/zone/test_case'
|
2
|
+
|
3
|
+
class RR_AAAA_Test < DNS::Zone::TestCase
|
4
|
+
|
5
|
+
def test_build_rr__aaaa
|
6
|
+
rr = DNS::Zone::RR::AAAA.new
|
7
|
+
|
8
|
+
# ensure we can set address parameter
|
9
|
+
rr.address = '2001:db8::3'
|
10
|
+
assert_equal 'AAAA', rr.type
|
11
|
+
assert_equal '@ IN AAAA 2001:db8::3', rr.dump
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_load_rr__aaaa
|
15
|
+
rr = DNS::Zone::RR::AAAA.new.load('@ IN AAAA 2001:db8::6')
|
16
|
+
assert_equal '@', rr.label
|
17
|
+
assert_equal 'AAAA', rr.type
|
18
|
+
assert_equal '2001:db8::6', rr.address
|
19
|
+
|
20
|
+
rr = DNS::Zone::RR::AAAA.new.load('www IN A 2001:db8::6')
|
21
|
+
assert_equal 'www', rr.label
|
22
|
+
assert_equal 'AAAA', rr.type
|
23
|
+
assert_equal 'IN', rr.klass
|
24
|
+
assert_equal '2001:db8::6', rr.address
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|