mongo 2.13.0.rc1 → 2.13.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
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/lib/mongo/auth/aws/request.rb +27 -3
- data/lib/mongo/client.rb +35 -0
- data/lib/mongo/error.rb +2 -0
- data/lib/mongo/error/invalid_server_auth_host.rb +22 -0
- data/lib/mongo/index/view.rb +3 -0
- data/lib/mongo/protocol/msg.rb +19 -0
- data/lib/mongo/server/app_metadata.rb +27 -3
- data/lib/mongo/server/connection_base.rb +34 -8
- data/lib/mongo/version.rb +1 -1
- data/spec/integration/bulk_write_spec.rb +19 -0
- data/spec/integration/client_side_encryption/auto_encryption_bulk_writes_spec.rb +3 -3
- data/spec/integration/size_limit_spec.rb +23 -10
- data/spec/mongo/auth/aws/request_region_spec.rb +42 -0
- data/spec/mongo/auth/aws/request_spec.rb +32 -32
- data/spec/mongo/client_construction_spec.rb +112 -0
- data/spec/mongo/index/view_spec.rb +146 -0
- data/spec/mongo/server/app_metadata_shared.rb +80 -0
- metadata +985 -984
- metadata.gz.sig +0 -0
- data/spec/integration/size_limit_spec.rb~12e1e9c4f... RUBY-2242 Fix zlib compression (#2021) +0 -98
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df3eb3a5733323f2f4440c67bc0173661ada194ecb35374e8e78345e2d072525
|
4
|
+
data.tar.gz: 63227bff6f14e3100f86a747b3e5d577812dd74d6da8061a069d8a313ecce09c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '02253019a9aab19c94dbb2a4e459a84c1fa6f1383eca9d5b1daa26bf1c2aa1a96c32f52a23267b0b7b5c58fc469144d0c1722b4f79593ae7563ac0c9deb55d95'
|
7
|
+
data.tar.gz: e02d93a95630d770f20a37cbe91a4dc18c98cb18e684da4b2a538a70afaaf61731e808832b2fe9aadca484dacfc8dce43cedf356ef00db019a03d77e9283cc45
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
@@ -61,9 +61,13 @@ module Mongo
|
|
61
61
|
%i(access_key_id secret_access_key host server_nonce).each do |arg|
|
62
62
|
value = instance_variable_get("@#{arg}")
|
63
63
|
if value.nil? || value.empty?
|
64
|
-
raise
|
64
|
+
raise Error::InvalidServerAuthResponse, "Value for '#{arg}' is required"
|
65
65
|
end
|
66
66
|
end
|
67
|
+
|
68
|
+
if host && host.length > 255
|
69
|
+
raise Error::InvalidServerAuthHost, "Value for 'host' is too long: #{@host}"
|
70
|
+
end
|
67
71
|
end
|
68
72
|
|
69
73
|
# @return [ String ] access_key_id The access key id.
|
@@ -98,8 +102,28 @@ module Mongo
|
|
98
102
|
|
99
103
|
# @return [ String ] region The region of the host, derived from the host.
|
100
104
|
def region
|
101
|
-
#
|
102
|
-
'
|
105
|
+
# Common case
|
106
|
+
if host == 'sts.amazonaws.com'
|
107
|
+
return 'us-east-1'
|
108
|
+
end
|
109
|
+
|
110
|
+
if host.start_with?('.')
|
111
|
+
raise Error::InvalidServerAuthHost, "Host begins with a period: #{host}"
|
112
|
+
end
|
113
|
+
if host.end_with?('.')
|
114
|
+
raise Error::InvalidServerAuthHost, "Host ends with a period: #{host}"
|
115
|
+
end
|
116
|
+
|
117
|
+
parts = host.split('.')
|
118
|
+
if parts.any? { |part| part.empty? }
|
119
|
+
raise Error::InvalidServerAuthHost, "Host has an empty component: #{host}"
|
120
|
+
end
|
121
|
+
|
122
|
+
if parts.length == 1
|
123
|
+
'us-east-1'
|
124
|
+
else
|
125
|
+
parts[1]
|
126
|
+
end
|
103
127
|
end
|
104
128
|
|
105
129
|
# Returns the scope of the request, per the AWS signature V4 specification.
|
data/lib/mongo/client.rb
CHANGED
@@ -104,6 +104,7 @@ module Mongo
|
|
104
104
|
:truncate_logs,
|
105
105
|
:user,
|
106
106
|
:wait_queue_timeout,
|
107
|
+
:wrapping_libraries,
|
107
108
|
:write,
|
108
109
|
:write_concern,
|
109
110
|
:zlib_compression_level,
|
@@ -375,6 +376,10 @@ module Mongo
|
|
375
376
|
# @option options [ String ] :user The user name.
|
376
377
|
# @option options [ Float ] :wait_queue_timeout The time to wait, in
|
377
378
|
# seconds, in the connection pool for a connection to be checked in.
|
379
|
+
# @option options [ Array<Hash> ] :wrapping_libraries Information about
|
380
|
+
# libraries such as ODMs that are wrapping the driver, to be added to
|
381
|
+
# metadata sent to the server. Specify the lower level libraries first.
|
382
|
+
# Allowed hash keys: :name, :version, :platform.
|
378
383
|
# @option options [ Hash ] :write Deprecated. Equivalent to :write_concern
|
379
384
|
# option.
|
380
385
|
# @option options [ Hash ] :write_concern The write concern options.
|
@@ -1160,6 +1165,36 @@ module Mongo
|
|
1160
1165
|
raise ArgumentError, ":bg_error_backtrace option value must be true, false, nil or a positive integer: #{value}"
|
1161
1166
|
end
|
1162
1167
|
end
|
1168
|
+
|
1169
|
+
if libraries = options[:wrapping_libraries]
|
1170
|
+
unless Array === libraries
|
1171
|
+
raise ArgumentError, ":wrapping_libraries must be an array of hashes: #{libraries}"
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
libraries = libraries.map do |library|
|
1175
|
+
Utils.shallow_symbolize_keys(library)
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
libraries.each do |library|
|
1179
|
+
unless Hash === library
|
1180
|
+
raise ArgumentError, ":wrapping_libraries element is not a hash: #{library}"
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
if library.empty?
|
1184
|
+
raise ArgumentError, ":wrapping_libraries element is empty"
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
unless (library.keys - %i(name platform version)).empty?
|
1188
|
+
raise ArgumentError, ":wrapping_libraries element has invalid keys (allowed keys: :name, :platform, :version): #{library}"
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
library.each do |key, value|
|
1192
|
+
if value.include?('|')
|
1193
|
+
raise ArgumentError, ":wrapping_libraries element value cannot include '|': #{value}"
|
1194
|
+
end
|
1195
|
+
end
|
1196
|
+
end
|
1197
|
+
end
|
1163
1198
|
end
|
1164
1199
|
|
1165
1200
|
# Validates all authentication-related options after they are set on the client
|
data/lib/mongo/error.rb
CHANGED
@@ -190,6 +190,8 @@ require 'mongo/error/invalid_application_name'
|
|
190
190
|
require 'mongo/error/invalid_nonce'
|
191
191
|
require 'mongo/error/invalid_replacement_document'
|
192
192
|
require 'mongo/error/invalid_server_auth_response'
|
193
|
+
# Subclass of InvalidServerAuthResponse
|
194
|
+
require 'mongo/error/invalid_server_auth_host'
|
193
195
|
require 'mongo/error/invalid_server_preference'
|
194
196
|
require 'mongo/error/invalid_session'
|
195
197
|
require 'mongo/error/invalid_signature'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (C) 2020 MongoDB Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
module Mongo
|
16
|
+
class Error
|
17
|
+
|
18
|
+
# Raised when the server returned an invalid Host value in AWS auth.
|
19
|
+
class InvalidServerAuthHost < InvalidServerAuthResponse
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/mongo/index/view.rb
CHANGED
@@ -120,6 +120,9 @@ module Mongo
|
|
120
120
|
# a geo index.
|
121
121
|
# @option options [ Hash ] :partial_filter_expression Specify a filter for a partial
|
122
122
|
# index.
|
123
|
+
# @option options [ Boolean ] :hidden When :hidden is true, this index will
|
124
|
+
# exist on the collection but not be used by the query planner when
|
125
|
+
# executing operations.
|
123
126
|
# @option options [ String | Integer ] :commit_quorum Specify how many
|
124
127
|
# data-bearing members of a replica set, including the primary, must
|
125
128
|
# complete the index builds successfully before the primary marks
|
data/lib/mongo/protocol/msg.rb
CHANGED
@@ -147,6 +147,8 @@ module Mongo
|
|
147
147
|
#
|
148
148
|
# @since 2.5.0
|
149
149
|
def serialize(buffer = BSON::ByteBuffer.new, max_bson_size = nil)
|
150
|
+
validate_document_size!(max_bson_size)
|
151
|
+
|
150
152
|
super
|
151
153
|
add_check_sum(buffer)
|
152
154
|
buffer
|
@@ -275,6 +277,23 @@ module Mongo
|
|
275
277
|
|
276
278
|
private
|
277
279
|
|
280
|
+
# Validate that the documents in this message are all smaller than the
|
281
|
+
# maxBsonObjectSize. If not, raise an exception.
|
282
|
+
def validate_document_size!(max_bson_size)
|
283
|
+
max_bson_size ||= Mongo::Server::ConnectionBase::DEFAULT_MAX_BSON_OBJECT_SIZE
|
284
|
+
|
285
|
+
contains_too_large_document = @sections.any? do |section|
|
286
|
+
section[:type] == 1 &&
|
287
|
+
section[:payload][:sequence].any? do |document|
|
288
|
+
document.to_bson.length > max_bson_size
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
if contains_too_large_document
|
293
|
+
raise Error::MaxBSONSize.new('The document exceeds maximum allowed BSON object size after serialization')
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
278
297
|
def command
|
279
298
|
@command ||= if @main_document
|
280
299
|
@main_document.dup.tap do |cmd|
|
@@ -65,12 +65,17 @@ module Mongo
|
|
65
65
|
# the metadata printed to the mongod logs upon establishing a connection
|
66
66
|
# in server versions >= 3.4.
|
67
67
|
# @option options [ String ] :user The user name.
|
68
|
+
# @option options [ Array<Hash> ] :wrapping_libraries Information about
|
69
|
+
# libraries such as ODMs that are wrapping the driver. Specify the
|
70
|
+
# lower level libraries first. Allowed hash keys: :name, :version,
|
71
|
+
# :platform.
|
68
72
|
#
|
69
73
|
# @since 2.4.0
|
70
74
|
def initialize(options)
|
71
75
|
@app_name = options[:app_name].to_s if options[:app_name]
|
72
76
|
@platform = options[:platform]
|
73
77
|
@compressors = options[:compressors] || []
|
78
|
+
@wrapping_libraries = options[:wrapping_libraries]
|
74
79
|
|
75
80
|
if options[:user] && !options[:auth_mech]
|
76
81
|
auth_db = options[:auth_source] || 'admin'
|
@@ -78,6 +83,10 @@ module Mongo
|
|
78
83
|
end
|
79
84
|
end
|
80
85
|
|
86
|
+
# @return [ Array<Hash> | nil ] Information about libraries wrapping
|
87
|
+
# the driver.
|
88
|
+
attr_reader :wrapping_libraries
|
89
|
+
|
81
90
|
# Get the bytes of the ismaster message including this metadata.
|
82
91
|
#
|
83
92
|
# @api private
|
@@ -140,9 +149,17 @@ module Mongo
|
|
140
149
|
end
|
141
150
|
|
142
151
|
def driver_doc
|
152
|
+
names = [DRIVER_NAME]
|
153
|
+
versions = [Mongo::VERSION]
|
154
|
+
if wrapping_libraries
|
155
|
+
wrapping_libraries.each do |library|
|
156
|
+
names << library[:name] || ''
|
157
|
+
versions << library[:version] || ''
|
158
|
+
end
|
159
|
+
end
|
143
160
|
{
|
144
|
-
name:
|
145
|
-
version:
|
161
|
+
name: names.join('|'),
|
162
|
+
version: versions.join('|'),
|
146
163
|
}
|
147
164
|
end
|
148
165
|
|
@@ -175,12 +192,19 @@ module Mongo
|
|
175
192
|
ruby_versions = ["Ruby #{RUBY_VERSION}"]
|
176
193
|
platforms = [RUBY_PLATFORM]
|
177
194
|
end
|
178
|
-
[
|
195
|
+
platform = [
|
179
196
|
@platform,
|
180
197
|
*ruby_versions,
|
181
198
|
*platforms,
|
182
199
|
RbConfig::CONFIG['build'],
|
183
200
|
].compact.join(', ')
|
201
|
+
platforms = [platform]
|
202
|
+
if wrapping_libraries
|
203
|
+
wrapping_libraries.each do |library|
|
204
|
+
platforms << library[:platform] || ''
|
205
|
+
end
|
206
|
+
end
|
207
|
+
platforms.join('|')
|
184
208
|
end
|
185
209
|
end
|
186
210
|
end
|
@@ -186,9 +186,6 @@ module Mongo
|
|
186
186
|
end
|
187
187
|
|
188
188
|
def serialize(message, client, buffer = BSON::ByteBuffer.new)
|
189
|
-
start_size = 0
|
190
|
-
final_message = message.maybe_compress(compressor, options[:zlib_compression_level])
|
191
|
-
|
192
189
|
# Driver specifications only mandate the fixed 16MiB limit for
|
193
190
|
# serialized BSON documents. However, the server returns its
|
194
191
|
# active serialized BSON document size limit in the ismaster response,
|
@@ -213,12 +210,41 @@ module Mongo
|
|
213
210
|
max_bson_size += MAX_BSON_COMMAND_OVERHEAD
|
214
211
|
end
|
215
212
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
213
|
+
# RUBY-2234: It is necessary to check that the message size does not
|
214
|
+
# exceed the maximum bson object size before compressing and serializing
|
215
|
+
# the final message.
|
216
|
+
#
|
217
|
+
# This is to avoid the case where the user performs a bulk write
|
218
|
+
# larger than 16MiB which, when compressed, becomes smaller than 16MiB.
|
219
|
+
# If the driver does not split the bulk writes prior to compression,
|
220
|
+
# the entire operation will be sent to the server, which will raise an
|
221
|
+
# error because the uncompressed operation exceeds the maximum bson size.
|
222
|
+
#
|
223
|
+
# To address this problem, we serialize the message prior to compression
|
224
|
+
# and raise an exception if the serialized message exceeds the maximum
|
225
|
+
# bson size.
|
226
|
+
if max_message_size
|
227
|
+
# Create a separate buffer that contains the un-compressed message
|
228
|
+
# for the purpose of checking its size. Write any pre-existing contents
|
229
|
+
# from the original buffer into the temporary one.
|
230
|
+
temp_buffer = BSON::ByteBuffer.new
|
231
|
+
|
232
|
+
# TODO: address the fact that this line mutates the buffer.
|
233
|
+
temp_buffer.put_bytes(buffer.get_bytes(buffer.length))
|
234
|
+
|
235
|
+
message.serialize(temp_buffer, max_bson_size)
|
236
|
+
if temp_buffer.length > max_message_size
|
237
|
+
raise Error::MaxMessageSize.new(max_message_size)
|
238
|
+
end
|
221
239
|
end
|
240
|
+
|
241
|
+
# RUBY-2335: When the un-compressed message is smaller than the maximum
|
242
|
+
# bson size limit, the message will be serialized twice. The operations
|
243
|
+
# layer should be refactored to allow compression on an already-
|
244
|
+
# serialized message.
|
245
|
+
final_message = message.maybe_compress(compressor, options[:zlib_compression_level])
|
246
|
+
final_message.serialize(buffer, max_bson_size)
|
247
|
+
|
222
248
|
buffer
|
223
249
|
end
|
224
250
|
end
|
data/lib/mongo/version.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Bulk writes' do
|
4
|
+
before do
|
5
|
+
authorized_collection.drop
|
6
|
+
end
|
7
|
+
|
8
|
+
context 'when bulk write is larger than 48MB' do
|
9
|
+
let(:operations) do
|
10
|
+
[ { insert_one: { text: 'a' * 1000 * 1000 } } ] * 48
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'succeeds' do
|
14
|
+
expect do
|
15
|
+
authorized_collection.bulk_write(operations)
|
16
|
+
end.not_to raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -113,7 +113,7 @@ describe 'Bulk writes with auto-encryption enabled' do
|
|
113
113
|
it 'raises an exception' do
|
114
114
|
expect do
|
115
115
|
bulk_write.execute
|
116
|
-
end.to raise_error(Mongo::Error::MaxBSONSize, /maximum allowed size
|
116
|
+
end.to raise_error(Mongo::Error::MaxBSONSize, /The document exceeds maximum allowed BSON object size after serialization/)
|
117
117
|
end
|
118
118
|
end
|
119
119
|
end
|
@@ -255,7 +255,7 @@ describe 'Bulk writes with auto-encryption enabled' do
|
|
255
255
|
it 'raises an exception' do
|
256
256
|
expect do
|
257
257
|
bulk_write.execute
|
258
|
-
end.to raise_error(Mongo::Error::MaxBSONSize, /maximum allowed size
|
258
|
+
end.to raise_error(Mongo::Error::MaxBSONSize, /The document exceeds maximum allowed BSON object size after serialization/)
|
259
259
|
end
|
260
260
|
end
|
261
261
|
end
|
@@ -346,7 +346,7 @@ describe 'Bulk writes with auto-encryption enabled' do
|
|
346
346
|
it 'raises an exception' do
|
347
347
|
expect do
|
348
348
|
perform_bulk_write
|
349
|
-
end.to raise_error(Mongo::Error::MaxBSONSize, /maximum allowed size
|
349
|
+
end.to raise_error(Mongo::Error::MaxBSONSize, /The document exceeds maximum allowed BSON object size after serialization/)
|
350
350
|
end
|
351
351
|
end
|
352
352
|
end
|
@@ -62,13 +62,30 @@ describe 'BSON & command size limits' do
|
|
62
62
|
authorized_collection.insert_one(document)
|
63
63
|
end
|
64
64
|
|
65
|
-
|
66
|
-
|
67
|
-
expect(document.to_bson.length).to eq(max_document_size+1)
|
65
|
+
context 'on server versions >= 3.6' do
|
66
|
+
min_server_fcv '3.6'
|
68
67
|
|
69
|
-
|
70
|
-
|
71
|
-
|
68
|
+
it 'fails on the driver when a document larger than 16MiB is inserted' do
|
69
|
+
document = { key: 'a' * (max_document_size - 27), _id: 'foo' }
|
70
|
+
expect(document.to_bson.length).to eq(max_document_size+1)
|
71
|
+
|
72
|
+
lambda do
|
73
|
+
authorized_collection.insert_one(document)
|
74
|
+
end.should raise_error(Mongo::Error::MaxBSONSize, /The document exceeds maximum allowed BSON object size after serialization/)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'on server versions <= 3.4' do
|
79
|
+
max_server_fcv '3.4'
|
80
|
+
|
81
|
+
it 'fails on the server when a document larger than 16MiB is inserted' do
|
82
|
+
document = { key: 'a' * (max_document_size - 27), _id: 'foo' }
|
83
|
+
expect(document.to_bson.length).to eq(max_document_size+1)
|
84
|
+
|
85
|
+
lambda do
|
86
|
+
authorized_collection.insert_one(document)
|
87
|
+
end.should raise_error(Mongo::Error::OperationFailure, /object to insert too large/)
|
88
|
+
end
|
72
89
|
end
|
73
90
|
|
74
91
|
it 'fails in the driver when a document larger than 16MiB+16KiB is inserted' do
|
@@ -81,10 +98,6 @@ describe 'BSON & command size limits' do
|
|
81
98
|
end
|
82
99
|
|
83
100
|
it 'allows bulk writes of multiple documents of exactly 16 MiB each' do
|
84
|
-
if SpecConfig.instance.compressors
|
85
|
-
pending "RUBY-2234"
|
86
|
-
end
|
87
|
-
|
88
101
|
documents = []
|
89
102
|
1.upto(3) do |index|
|
90
103
|
document = { key: 'a' * (max_document_size - 28), _id: "in#{index}" }
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'lite_spec_helper'
|
2
|
+
|
3
|
+
AWS_REGION_TEST_CASES = {
|
4
|
+
'sts.amazonaws.com' => 'us-east-1',
|
5
|
+
'sts.us-west-2.amazonaws.com' => 'us-west-2',
|
6
|
+
'sts.us-west-2.amazonaws.com.ch' => 'us-west-2',
|
7
|
+
'example.com' => 'com',
|
8
|
+
'localhost' => 'us-east-1',
|
9
|
+
'sts..com' => Mongo::Error::InvalidServerAuthHost,
|
10
|
+
'.amazonaws.com' => Mongo::Error::InvalidServerAuthHost,
|
11
|
+
'sts.amazonaws.' => Mongo::Error::InvalidServerAuthHost,
|
12
|
+
'' => Mongo::Error::InvalidServerAuthResponse,
|
13
|
+
'x' * 256 => Mongo::Error::InvalidServerAuthHost,
|
14
|
+
}
|
15
|
+
|
16
|
+
describe 'AWS auth region tests' do
|
17
|
+
|
18
|
+
AWS_REGION_TEST_CASES.each do |host, expected_region|
|
19
|
+
context "host '#{host}'" do
|
20
|
+
let(:request) do
|
21
|
+
Mongo::Auth::Aws::Request.new(access_key_id: 'access_key_id',
|
22
|
+
secret_access_key: 'secret_access_key',
|
23
|
+
session_token: 'session_token',
|
24
|
+
host: host,
|
25
|
+
server_nonce: 'server_nonce',
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
if expected_region.is_a?(String)
|
30
|
+
it 'derives expected region' do
|
31
|
+
request.region.should == expected_region
|
32
|
+
end
|
33
|
+
else
|
34
|
+
it 'fails with an error' do
|
35
|
+
lambda do
|
36
|
+
request.region
|
37
|
+
end.should raise_error(expected_region)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|