dns-zone 0.1.3 → 0.2.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 +4 -4
- data/HISTORY.md +19 -0
- data/README.md +49 -11
- data/dns-zone.gemspec +3 -3
- data/lib/dns/zone.rb +97 -13
- data/lib/dns/zone/rr.rb +50 -25
- data/lib/dns/zone/rr/cdnskey.rb +5 -0
- data/lib/dns/zone/rr/cds.rb +5 -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 +4 -8
- data/lib/dns/zone/rr/naptr.rb +44 -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/rrsig.rb +54 -0
- data/lib/dns/zone/rr/sshfp.rb +35 -0
- data/lib/dns/zone/version.rb +1 -1
- data/test/rr/cdnskey_test.rb +31 -0
- data/test/rr/cds_test.rb +28 -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/naptr_test.rb +60 -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/rrsig_test.rb +40 -0
- data/test/rr/sshfp_test.rb +24 -0
- data/test/zone_test.rb +32 -0
- metadata +29 -8
- data/Guardfile +0 -14
@@ -0,0 +1,38 @@
|
|
1
|
+
# `DNSKEY` resource record.
|
2
|
+
#
|
3
|
+
# RFC 4034
|
4
|
+
class DNS::Zone::RR::DNSKEY < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_DNSKEY_RDATA = %r{
|
7
|
+
(?<flags>\d+)\s*
|
8
|
+
(?<protocol>\d+)\s*
|
9
|
+
(?<algorithm>\d+)\s*
|
10
|
+
(?<key>#{DNS::Zone::RR::REGEX_STRING})\s*
|
11
|
+
}mx
|
12
|
+
|
13
|
+
attr_accessor :flags, :protocol, :algorithm, :key
|
14
|
+
|
15
|
+
def dump
|
16
|
+
parts = general_prefix
|
17
|
+
parts << flags
|
18
|
+
parts << protocol
|
19
|
+
parts << algorithm
|
20
|
+
parts << key
|
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_DNSKEY_RDATA)
|
29
|
+
return nil unless captures
|
30
|
+
|
31
|
+
@flags = captures[:flags].to_i
|
32
|
+
@protocol = captures[:protocol].to_i
|
33
|
+
@algorithm = captures[:algorithm].to_i
|
34
|
+
@key = captures[:key].scan(/#{DNS::Zone::RR::REGEX_STRING}/).join
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# `DS` resource record.
|
2
|
+
#
|
3
|
+
# RFC 4034
|
4
|
+
class DNS::Zone::RR::DS < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_DS_RDATA = %r{
|
7
|
+
(?<key_tag>\d+)\s*
|
8
|
+
(?<algorithm>\d+)\s*
|
9
|
+
(?<digest_type>\d+)\s*
|
10
|
+
(?<digest>#{DNS::Zone::RR::REGEX_STRING})\s*
|
11
|
+
}mx
|
12
|
+
|
13
|
+
attr_accessor :key_tag, :algorithm, :digest_type, :digest
|
14
|
+
|
15
|
+
def dump
|
16
|
+
parts = general_prefix
|
17
|
+
parts << key_tag
|
18
|
+
parts << algorithm
|
19
|
+
parts << digest_type
|
20
|
+
parts << digest
|
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_DS_RDATA)
|
29
|
+
return nil unless captures
|
30
|
+
|
31
|
+
@key_tag = captures[:key_tag].to_i
|
32
|
+
@algorithm = captures[:algorithm].to_i
|
33
|
+
@digest_type = captures[:digest_type].to_i
|
34
|
+
@digest = captures[:digest].scan(/#{DNS::Zone::RR::REGEX_STRING}/).join
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/dns/zone/rr/hinfo.rb
CHANGED
@@ -3,13 +3,9 @@
|
|
3
3
|
# RFC 1035
|
4
4
|
class DNS::Zone::RR::HINFO < DNS::Zone::RR::Record
|
5
5
|
|
6
|
-
REGEX_QUOTES_OPTIONAL = %r{
|
7
|
-
"#{DNS::Zone::RR::REGEX_STRING}"|#{DNS::Zone::RR::REGEX_STRING}
|
8
|
-
}mx
|
9
|
-
|
10
6
|
REGEX_HINFO_RDATA = %r{
|
11
|
-
(?<cpu>(?:#{
|
12
|
-
(?<os>(?:#{
|
7
|
+
(?<cpu>(?:#{DNS::Zone::RR::REGEX_CHARACTER_STRING})){1}\s
|
8
|
+
(?<os>(?:#{DNS::Zone::RR::REGEX_CHARACTER_STRING})){1}
|
13
9
|
}mx
|
14
10
|
attr_accessor :cpu
|
15
11
|
attr_accessor :os
|
@@ -27,8 +23,8 @@ class DNS::Zone::RR::HINFO < DNS::Zone::RR::Record
|
|
27
23
|
captures = rdata.match(REGEX_HINFO_RDATA)
|
28
24
|
return nil unless captures
|
29
25
|
|
30
|
-
@cpu = captures[:cpu].scan(/#{
|
31
|
-
@os = captures[:os].scan(
|
26
|
+
@cpu = captures[:cpu].scan(/#{DNS::Zone::RR::REGEX_CHARACTER_STRING}/).join
|
27
|
+
@os = captures[:os].scan(/#{DNS::Zone::RR::REGEX_CHARACTER_STRING}/).join
|
32
28
|
self
|
33
29
|
end
|
34
30
|
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# `NAPTR` resource record.
|
2
|
+
#
|
3
|
+
# RFC 3403
|
4
|
+
class DNS::Zone::RR::NAPTR < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_NAPTR_RDATA = %r{
|
7
|
+
(?<order>\d+)\s*
|
8
|
+
(?<pref>\d+)\s*
|
9
|
+
(?<flags>#{DNS::Zone::RR::REGEX_CHARACTER_STRING})\s*
|
10
|
+
(?<service>#{DNS::Zone::RR::REGEX_CHARACTER_STRING})\s*
|
11
|
+
(?<regexp>#{DNS::Zone::RR::REGEX_CHARACTER_STRING})\s*
|
12
|
+
(?<replacement>#{DNS::Zone::RR::REGEX_DOMAINNAME}|\.{1})\s*
|
13
|
+
}mx
|
14
|
+
|
15
|
+
attr_accessor :order, :pref, :flags, :service, :regexp, :replacement
|
16
|
+
|
17
|
+
def dump
|
18
|
+
parts = general_prefix
|
19
|
+
parts << order
|
20
|
+
parts << pref
|
21
|
+
parts << %Q{"#{flags}"}
|
22
|
+
parts << %Q{"#{service}"}
|
23
|
+
parts << %Q{"#{regexp}"}
|
24
|
+
parts << replacement
|
25
|
+
parts.join(' ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def load(string, options = {})
|
29
|
+
rdata = load_general_and_get_rdata(string, options)
|
30
|
+
return nil unless rdata
|
31
|
+
|
32
|
+
captures = rdata.match(REGEX_NAPTR_RDATA)
|
33
|
+
return nil unless captures
|
34
|
+
|
35
|
+
@order = captures[:order].to_i
|
36
|
+
@pref = captures[:pref].to_i
|
37
|
+
@flags = captures[:flags].scan(/#{DNS::Zone::RR::REGEX_CHARACTER_STRING}/).join
|
38
|
+
@service = captures[:service].scan(/#{DNS::Zone::RR::REGEX_CHARACTER_STRING}/).join
|
39
|
+
@regexp = captures[:regexp].scan(/#{DNS::Zone::RR::REGEX_CHARACTER_STRING}/).join
|
40
|
+
@replacement = captures[:replacement]
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# `NSEC` resource record.
|
2
|
+
#
|
3
|
+
# RFC 4034
|
4
|
+
class DNS::Zone::RR::NSEC < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_NSEC_RDATA = %r{
|
7
|
+
(?<next_domain>#{DNS::Zone::RR::REGEX_DOMAINNAME})\s*
|
8
|
+
(?<rrset_types>#{DNS::Zone::RR::REGEX_STRING})\s*
|
9
|
+
}mx
|
10
|
+
|
11
|
+
attr_accessor :next_domain, :rrset_types
|
12
|
+
|
13
|
+
def dump
|
14
|
+
parts = general_prefix
|
15
|
+
parts << next_domain
|
16
|
+
parts << rrset_types
|
17
|
+
parts.join(' ')
|
18
|
+
end
|
19
|
+
|
20
|
+
def load(string, options = {})
|
21
|
+
rdata = load_general_and_get_rdata(string, options)
|
22
|
+
return nil unless rdata
|
23
|
+
|
24
|
+
captures = rdata.match(REGEX_NSEC_RDATA)
|
25
|
+
return nil unless captures
|
26
|
+
|
27
|
+
@next_domain = captures[:next_domain]
|
28
|
+
@rrset_types = captures[:rrset_types]
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# `NSEC3` resource record.
|
2
|
+
#
|
3
|
+
# RFC 5155
|
4
|
+
class DNS::Zone::RR::NSEC3 < DNS::Zone::RR::Record
|
5
|
+
|
6
|
+
REGEX_NSEC3_RDATA = %r{
|
7
|
+
(?<algorithm>\d+)\s*
|
8
|
+
(?<flags>\d+)\s*
|
9
|
+
(?<iterations>\d+)\s*
|
10
|
+
(?<salt>\S+)\s*
|
11
|
+
(?<next_hashed_owner_name>\S+)\s*
|
12
|
+
(?<rrset_types>#{DNS::Zone::RR::REGEX_STRING})\s*
|
13
|
+
}mx
|
14
|
+
|
15
|
+
attr_accessor :algorithm, :flags, :iterations, :salt,
|
16
|
+
:next_hashed_owner_name, :rrset_types
|
17
|
+
|
18
|
+
def dump
|
19
|
+
parts = general_prefix
|
20
|
+
parts << algorithm
|
21
|
+
parts << flags
|
22
|
+
parts << iterations
|
23
|
+
parts << salt
|
24
|
+
parts << next_hashed_owner_name
|
25
|
+
parts << rrset_types
|
26
|
+
parts.join(' ')
|
27
|
+
end
|
28
|
+
|
29
|
+
def load(string, options = {})
|
30
|
+
rdata = load_general_and_get_rdata(string, options)
|
31
|
+
return nil unless rdata
|
32
|
+
|
33
|
+
captures = rdata.match(REGEX_NSEC3_RDATA)
|
34
|
+
return nil unless captures
|
35
|
+
|
36
|
+
@algorithm = captures[:algorithm].to_i
|
37
|
+
@flags = captures[:flags].to_i
|
38
|
+
@iterations = captures[:iterations].to_i
|
39
|
+
@salt = captures[:salt]
|
40
|
+
@next_hashed_owner_name = captures[:next_hashed_owner_name]
|
41
|
+
@rrset_types = captures[:rrset_types]
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -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,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,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
|
data/lib/dns/zone/version.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'dns/zone/test_case'
|
2
|
+
|
3
|
+
class RR_CDNSKEY_Test < DNS::Zone::TestCase
|
4
|
+
|
5
|
+
# FIXME (lantins): algorithm can be integer _or_ mnemonic!
|
6
|
+
|
7
|
+
TEST_KEY = 'AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w=='
|
8
|
+
|
9
|
+
def test_build_rr__cdnskey
|
10
|
+
rr = DNS::Zone::RR::CDNSKEY.new
|
11
|
+
rr.label = 'example.com.'
|
12
|
+
rr.ttl = 86400
|
13
|
+
rr.flags = 256
|
14
|
+
rr.protocol = 3
|
15
|
+
rr.algorithm = 5
|
16
|
+
rr.key = TEST_KEY
|
17
|
+
|
18
|
+
assert_equal "example.com. 86400 IN CDNSKEY 256 3 5 #{TEST_KEY}", rr.dump
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_load_rr__cdnskey
|
22
|
+
rr = DNS::Zone::RR::CDNSKEY.new.load("example.com. IN CDNSKEY 256 3 5 #{TEST_KEY}")
|
23
|
+
assert_equal 'example.com.', rr.label
|
24
|
+
assert_equal 'CDNSKEY', rr.type
|
25
|
+
assert_equal 256, rr.flags
|
26
|
+
assert_equal 3, rr.protocol
|
27
|
+
assert_equal 5, rr.algorithm
|
28
|
+
assert_equal TEST_KEY, rr.key
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
data/test/rr/cds_test.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'dns/zone/test_case'
|
2
|
+
|
3
|
+
class RR_CDS_Test < DNS::Zone::TestCase
|
4
|
+
|
5
|
+
TEST_DIGEST = '2BB183AF5F22588179A53B0A98631FAD1A292118'
|
6
|
+
|
7
|
+
def test_build_rr__cds
|
8
|
+
rr = DNS::Zone::RR::CDS.new
|
9
|
+
rr.label = 'dskey.example.com.'
|
10
|
+
rr.key_tag = 60485
|
11
|
+
rr.algorithm = 5
|
12
|
+
rr.digest_type = 1
|
13
|
+
rr.digest = TEST_DIGEST
|
14
|
+
|
15
|
+
assert_equal "dskey.example.com. IN CDS 60485 5 1 #{TEST_DIGEST}", rr.dump
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_load_rr__cds
|
19
|
+
rr = DNS::Zone::RR::CDS.new.load("dskey.example.com. IN CDS 60485 5 1 #{TEST_DIGEST}")
|
20
|
+
assert_equal 'dskey.example.com.', rr.label
|
21
|
+
assert_equal 'CDS', rr.type
|
22
|
+
assert_equal 60485, rr.key_tag
|
23
|
+
assert_equal 5, rr.algorithm
|
24
|
+
assert_equal 1, rr.digest_type
|
25
|
+
assert_equal TEST_DIGEST, rr.digest
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/test/rr/dlv_test.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'dns/zone/test_case'
|
2
|
+
|
3
|
+
class RR_DLV_Test < DNS::Zone::TestCase
|
4
|
+
|
5
|
+
TEST_DIGEST = '2BB183AF5F22588179A53B0A98631FAD1A292118'
|
6
|
+
|
7
|
+
def test_build_rr__dlv
|
8
|
+
rr = DNS::Zone::RR::DLV.new
|
9
|
+
rr.label = 'dlvtest.example.com.'
|
10
|
+
rr.key_tag = 60485
|
11
|
+
rr.algorithm = 5
|
12
|
+
rr.digest_type = 1
|
13
|
+
rr.digest = TEST_DIGEST
|
14
|
+
|
15
|
+
assert_equal "dlvtest.example.com. IN DLV 60485 5 1 #{TEST_DIGEST}", rr.dump
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_load_rr__dlv
|
19
|
+
rr = DNS::Zone::RR::DLV.new.load("dlvtest.example.com. IN DLV 60485 5 1 #{TEST_DIGEST}")
|
20
|
+
assert_equal 'dlvtest.example.com.', rr.label
|
21
|
+
assert_equal 'DLV', rr.type
|
22
|
+
assert_equal 60485, rr.key_tag
|
23
|
+
assert_equal 5, rr.algorithm
|
24
|
+
assert_equal 1, rr.digest_type
|
25
|
+
assert_equal TEST_DIGEST, rr.digest
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|