op-clamav-client 3.4.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.
@@ -0,0 +1,129 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'timeout'
18
+
19
+ module ClamAV
20
+ class Connection
21
+ class ReadTimeoutError < ::ClamAV::Client::ReadTimeoutError; end
22
+ class WriteTimeoutError < ::ClamAV::Client::WriteTimeoutError; end
23
+
24
+ attr_accessor :client
25
+ attr_accessor :socket
26
+ attr_accessor :wrapper
27
+
28
+ def initialize(attrs={})
29
+ attrs.each do |attr, value|
30
+ send("#{attr}=", value)
31
+ end
32
+
33
+ begin
34
+ validate!
35
+ rescue => e
36
+ @client = nil
37
+ @socket = nil
38
+ @wrapper = nil
39
+
40
+ raise e
41
+ end
42
+ end
43
+
44
+ def validate!
45
+ missing_required_argument(:client) if !client
46
+ missing_required_argument(:socket) if !socket
47
+ missing_required_argument(:wrapper) if !wrapper
48
+ end
49
+
50
+ [
51
+ :tcp?,
52
+ :file,
53
+ :connect_timeout,
54
+ :read_timeout,
55
+ :write_timeout,
56
+ ].each do |m|
57
+ define_method(m) do
58
+ client.send(m)
59
+ end
60
+ end
61
+
62
+ def establish_connection
63
+ write_request("IDSESSION")
64
+ end
65
+
66
+ def write_request(str)
67
+ return write_request_with_timeout(str) if write_timeout
68
+
69
+ write_request_without_timeout(str)
70
+ end
71
+
72
+ def write_request_without_timeout(str)
73
+ wrapped_request = @wrapper.wrap_request(str)
74
+
75
+ @socket.write wrapped_request
76
+ end
77
+
78
+ def write_request_with_timeout(str)
79
+ Timeout::timeout(connect_timeout) do
80
+ write_request_without_timeout(str)
81
+ end
82
+ rescue Timeout::Error => e
83
+
84
+ raise WriteTimeoutError.new(e.to_s)
85
+ end
86
+
87
+ def read_response
88
+ return read_response_with_timeout if read_timeout
89
+
90
+ read_response_without_timeout
91
+ end
92
+
93
+ def read_response_without_timeout
94
+ @wrapper.read_response(@socket)
95
+ end
96
+
97
+ def read_response_with_timeout
98
+ Timeout::timeout(read_timeout) do
99
+ read_response_without_timeout
100
+ end
101
+ rescue Timeout::Error => e
102
+ raise ReadTimeoutError.new(e.to_s)
103
+ end
104
+
105
+ def disconnect!
106
+ return true if @socket.nil?
107
+
108
+ @socket.close
109
+
110
+ @socket.closed?.tap do
111
+ @socket = nil
112
+ end
113
+ end
114
+
115
+ def send_request(str)
116
+ write_request(str)
117
+ read_response
118
+ end
119
+
120
+ def raw_write(str)
121
+ @socket.write str
122
+ end
123
+
124
+ private
125
+ def missing_required_argument(key)
126
+ raise ArgumentError, "#{key} is required"
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,28 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ class Response
19
+
20
+ attr_reader :file, :virus_name, :error_str
21
+
22
+ # Not sure if this is still required?
23
+ def ==(other)
24
+ @file == other.file && self.class == other.class
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ class ErrorResponse < Response
19
+
20
+ def initialize(error_str)
21
+ @error_str = error_str
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ class SuccessResponse < Response
19
+
20
+ def initialize(file)
21
+ @file = file
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ class VirusResponse < Response
19
+
20
+ def initialize(file, virus_name)
21
+ @file = file
22
+ @virus_name = virus_name
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ module Util
19
+ UnknownPathException = Class.new(RuntimeError)
20
+
21
+ def Util.path_to_files(path)
22
+ if Dir.exist?(path)
23
+ Dir.glob(path + '/*')
24
+ elsif File.exist?(path)
25
+ [path]
26
+ else
27
+ message =" (path = #{path})"
28
+ raise UnknownPathException.new("#{__FILE__}:#{__LINE__} path_to_files : path argument neither a file nor a directory. Aborting. #{message}")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ class Wrapper
19
+ def wrap_request(request); raise NotImplementedError; end
20
+ def unwrap_response(response); raise NotImplementedError; end
21
+
22
+ protected
23
+ def read_until(socket, delimiter)
24
+ buff = ""
25
+ while (char = socket.getc) != delimiter
26
+ buff << char
27
+ end
28
+ buff
29
+ rescue TypeError => e
30
+ raise ::ClamAV::Client::ConnectionError.new(e.to_s)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ module Wrappers
19
+ class NewLineWrapper < ::ClamAV::Wrapper
20
+ def wrap_request(request)
21
+ "n#{request}\n"
22
+ end
23
+
24
+ def read_response(socket)
25
+ read_until(socket, "\n")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ module ClamAV
18
+ module Wrappers
19
+ class NullTerminationWrapper < ::ClamAV::Wrapper
20
+ def wrap_request(request)
21
+ "z#{request}\0"
22
+ end
23
+
24
+ def read_response(socket)
25
+ read_until(socket, "\0")
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/clamav.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative 'clamav/client'
data/test/Dockerfile ADDED
@@ -0,0 +1,16 @@
1
+ ARG RUBY_VERSION
2
+ FROM ruby:${RUBY_VERSION}
3
+ LABEL maintainer="victor.pugin@lifen.fr"
4
+
5
+ WORKDIR /clamav-client
6
+ ADD Gemfile /clamav-client
7
+ ADD clamav-client.gemspec /clamav-client
8
+
9
+ RUN apt-get update -qq && \
10
+ apt-get install -y clamav-daemon clamav-freshclam clamav-unofficial-sigs && \
11
+ freshclam && \
12
+ bundle
13
+
14
+ ENTRYPOINT ["./start.sh"]
15
+
16
+ ADD . /clamav-client
@@ -0,0 +1,142 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'test_helper'
18
+
19
+ describe "ClamAV::Client Integration Tests" do
20
+ describe "default new line delimiter" do
21
+ let(:client) { ClamAV::Client.new }
22
+
23
+ describe "any callable" do
24
+ it "can be used" do
25
+ assert client.execute(lambda { |conn| true })
26
+ end
27
+ end
28
+
29
+ describe "ping" do
30
+ let(:ping_command) { ClamAV::Commands::PingCommand.new }
31
+
32
+ it "can be sent" do
33
+ assert client.execute(ping_command)
34
+ end
35
+
36
+ it "can be sent multiple times" do
37
+ assert client.execute(ping_command)
38
+ assert client.execute(ping_command)
39
+ end
40
+
41
+ it "can be used as #ping" do
42
+ assert_equal client.execute(ping_command), client.ping
43
+ end
44
+ end
45
+
46
+ describe "scan" do
47
+ let(:base_path) { File.expand_path('../../../../', __FILE__) }
48
+ let(:dir) { File.join(base_path, 'test/fixtures') }
49
+
50
+ it "can be started" do
51
+ results = client.execute(ClamAV::Commands::ScanCommand.new(dir))
52
+
53
+ expected_results = {
54
+ "#{base_path}/test/fixtures/clamavtest.gz" => ClamAV::VirusResponse,
55
+ "#{base_path}/test/fixtures/clamavtest.txt" => ClamAV::VirusResponse,
56
+ "#{base_path}/test/fixtures/clamavtest.zip" => ClamAV::VirusResponse,
57
+ "#{base_path}/test/fixtures/innocent.txt" => ClamAV::SuccessResponse
58
+ }
59
+
60
+ results.each do |result|
61
+ expected_result = expected_results[result.file]
62
+ assert_equal expected_result, result.class
63
+ end
64
+ end
65
+
66
+ it "can be used as #scan" do
67
+ assert_equal client.execute(ClamAV::Commands::ScanCommand.new(dir)), client.send(:scan, dir)
68
+ end
69
+
70
+ describe "with timeout configuration" do
71
+ let(:client) { ClamAV::Client.new(connect_timeout: 1, write_timeout: 1, read_timeout: 1) }
72
+
73
+ it "can be started" do
74
+ results = client.execute(ClamAV::Commands::ScanCommand.new(dir))
75
+
76
+ expected_results = {
77
+ "#{base_path}/test/fixtures/clamavtest.gz" => ClamAV::VirusResponse,
78
+ "#{base_path}/test/fixtures/clamavtest.txt" => ClamAV::VirusResponse,
79
+ "#{base_path}/test/fixtures/clamavtest.zip" => ClamAV::VirusResponse,
80
+ "#{base_path}/test/fixtures/innocent.txt" => ClamAV::SuccessResponse
81
+ }
82
+
83
+ results.each do |result|
84
+ expected_result = expected_results[result.file]
85
+ assert_equal expected_result, result.class
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ describe "instream" do
92
+ let(:dir) { File.expand_path('../../../../test/fixtures', __FILE__) }
93
+
94
+ it "can recognize a sane file" do
95
+ command = build_command_for_file('innocent.txt')
96
+ client.execute(command).must_equal ClamAV::SuccessResponse.new("stream")
97
+ end
98
+
99
+ it "can recognize an infected file" do
100
+ command = build_command_for_file('clamavtest.txt')
101
+ client.execute(command).must_equal ClamAV::VirusResponse.new("stream", "ClamAV-Test-Signature")
102
+ end
103
+
104
+ it "can be used as #instream" do
105
+ io = File.open(File.join(dir, 'innocent.txt'))
106
+ assert_equal client.execute(ClamAV::Commands::InstreamCommand.new(io)), client.send(:instream, io)
107
+ end
108
+
109
+ def build_command_for_file(file)
110
+ io = File.open(File.join(dir, file))
111
+ ClamAV::Commands::InstreamCommand.new(io)
112
+ end
113
+ end
114
+
115
+ describe 'safe?' do
116
+ let(:dir) { File.expand_path('../../../../test/fixtures', __FILE__) }
117
+
118
+ it 'returns true if the given io is safe' do
119
+ io = build_io_obj('innocent.txt')
120
+ assert client.safe?(io)
121
+ end
122
+
123
+ it 'returns false if the given io is infected' do
124
+ io = build_io_obj('clamavtest.txt')
125
+ refute client.safe?(io)
126
+ end
127
+
128
+ it 'returns false if there is any infected file in the given files' do
129
+ refute client.safe?(dir)
130
+ end
131
+
132
+ it 'returns true if all the give file is safe' do
133
+ assert client.safe?("#{dir}/innocent.txt")
134
+ end
135
+
136
+ def build_io_obj(file)
137
+ content = File.read(File.join(dir, file))
138
+ io = StringIO.new(content)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,41 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'test_helper'
18
+ describe "ClamAV::Client Integration Tests" do
19
+ describe "Util" do
20
+ describe "absolute_path" do
21
+ it "transforms a single file to an array of one element" do
22
+ expected_path = File.absolute_path(__FILE__)
23
+ actual_path = ClamAV::Util.path_to_files(__FILE__).first
24
+ assert_equal expected_path, actual_path
25
+ end
26
+
27
+ it "transforms a directory to an array of N element" do
28
+ files = %w(
29
+ clamav-client/test/integration/clamav/client_test.rb
30
+ clamav-client/test/integration/clamav/util_test.rb
31
+ ).sort
32
+ actual_files = ClamAV::Util.path_to_files(File.dirname(__FILE__)).sort
33
+
34
+ files.each_with_index do |file, index|
35
+ path = Regexp.new(".*/#{file}")
36
+ assert path.match(actual_files[index])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ gem 'minitest'
18
+
19
+ require 'minitest/autorun'
20
+ require 'clamav/client'
21
+ require 'pry'
@@ -0,0 +1,59 @@
1
+ # clamav-client - ClamAV client
2
+ # Copyright (C) 2014 Franck Verrot <franck@verrot.fr>
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'test_helper'
18
+
19
+ describe "ClamAV::Client" do
20
+ describe "connect!" do
21
+ let(:conn_mock) { Minitest::Mock.new }
22
+ let(:client) { ClamAV::Client.new }
23
+
24
+ it "opens the connection" do
25
+ conn_mock.expect(:establish_connection, nil)
26
+
27
+ client.connect!(conn_mock)
28
+
29
+ conn_mock.verify
30
+ end
31
+
32
+ it 'raises an custom error if connection times out' do
33
+ conn_mock.expect(:establish_connection, nil) do
34
+ raise Errno::ETIMEDOUT
35
+ end
36
+
37
+ assert_raises(ClamAV::Client::ConnectTimeoutError) { client.connect!(conn_mock) }
38
+ end
39
+
40
+ it 'raises an custom error if something goes wrong' do
41
+ conn_mock.expect(:establish_connection, nil) do
42
+ raise SocketError
43
+ end
44
+
45
+ assert_raises(ClamAV::Client::ConnectionError) { client.connect!(conn_mock) }
46
+ end
47
+ end
48
+
49
+ describe "tcp?" do
50
+ it "returns true when config is tcp" do
51
+ assert client = ClamAV::Client.new(tcp_host: 'example', tcp_port: 3310).tcp?
52
+ end
53
+
54
+ it "returns false when config is not tcp" do
55
+ refute client = ClamAV::Client.new.tcp?
56
+ refute client = ClamAV::Client.new(unix_socket: '/some.sock').tcp?
57
+ end
58
+ end
59
+ end