@@ -44,11 +44,16 @@ class URI
4444 # @since 2.0.0
4545 attr_reader :servers
4646
47- # Unsafe characters that must be URI-escaped .
47+ # Unsafe characters that must be urlencoded .
4848 #
4949 # @since 2.1.0
5050 UNSAFE = /[\: \/ \+ \@ ]/
5151
52+ # Unix socket suffix.
53+ #
54+ # @since 2.1.0
55+ UNIX_SOCKET = /.sock/
56+
5257 # The mongodb connection string scheme.
5358 #
5459 # @since 2.0.0
@@ -67,7 +72,7 @@ class URI
6772 # The character delimiting a database.
6873 #
6974 # @since 2.1.0
70- DATABSE_DELIM = '/' . freeze
75+ DATABASE_DELIM = '/' . freeze
7176
7277 # The character delimiting options.
7378 #
@@ -105,30 +110,35 @@ class URI
105110 INVALID_OPTS_VALUE_DELIM = "Options and their values must be delimited" +
106111 " by '#{ URI_OPTS_VALUE_DELIM } '" . freeze
107112
108- # Error details for an un-escaped user name or password.
113+ # Error details for an non-urlencoded user name or password.
109114 #
110115 # @since 2.1.0
111- UNESCAPED_USER_PWD = "User name and password must be URI-escaped ." . freeze
116+ UNESCAPED_USER_PWD = "User name and password must be urlencoded ." . freeze
112117
113- # Error details for a non-delimited database name .
118+ # Error details for a non-urlencoded unix socket path .
114119 #
115120 # @since 2.1.0
116- INVALID_DB_DELIM = "Database must be delimited by a #{ DATABSE_DELIM } ." . freeze
121+ UNESCAPED_UNIX_SOCKET = "UNIX domain sockets must be urlencoded ." . freeze
117122
118- # Error details for a missing host .
123+ # Error details for a non-urlencoded auth databsae name .
119124 #
120125 # @since 2.1.0
121- INVALID_HOST = "At least one host must be specified ." . freeze
126+ UNESCAPED_DATABASE = "Auth database must be urlencoded ." . freeze
122127
123- # Error details for an invalid port .
128+ # Error details for providing options without a database delimiter .
124129 #
125130 # @since 2.1.0
126- INVALID_PORT = "Invalid port. Port must be greater than 0 and less than 65536 " . freeze
131+ INVALID_OPTS_DELIM = "Database delimiter ' #{ DATABASE_DELIM } ' must be present if options are specified. " . freeze
127132
128- # Error details for an invalid host:port format .
133+ # Error details for a missing host.
129134 #
130135 # @since 2.1.0
131- INVALID_HOST_PORT = "Invalid host:port format." . freeze
136+ INVALID_HOST = "Missing host; at least one must be provided." . freeze
137+
138+ # Error details for an invalid port.
139+ #
140+ # @since 2.1.0
141+ INVALID_PORT = "Invalid port. Port must be an integer greater than 0 and less than 65536" . freeze
132142
133143 # MongoDB URI format specification.
134144 #
@@ -161,6 +171,11 @@ class URI
161171 'GSSAPI' => :gssapi
162172 } . freeze
163173
174+ # Options that are allowed to appear more than once in the uri.
175+ #
176+ # @since 2.1.0
177+ REPEATABLE_OPTIONS = [ :tag_sets ]
178+
164179 # Create the new uri from the provided string.
165180 #
166181 # @example Create the new URI.
@@ -175,8 +190,8 @@ class URI
175190 def initialize ( string , options = { } )
176191 @string = string
177192 @options = options
178- remaining = @string . split ( SCHEME ) [ 1 ]
179- raise_invalid_error! ( INVALID_SCHEME ) unless remaining
193+ empty , scheme , remaining = @string . partition ( SCHEME )
194+ raise_invalid_error! ( INVALID_SCHEME ) unless scheme == SCHEME
180195 setup! ( remaining )
181196 end
182197
@@ -218,14 +233,48 @@ def credentials
218233 #
219234 # @since 2.0.0
220235 def database
221- @database ? :: URI . decode ( @database ) : Database ::ADMIN
236+ @database ? @database : Database ::ADMIN
222237 end
223238
224239 private
225240
226- def parse_uri_options! ( part , remaining )
227- return { } unless part
228- part . split ( INDIV_URI_OPTS_DELIM ) . reduce ( { } ) do |uri_options , opt |
241+ def setup! ( remaining )
242+ creds_hosts , db_opts = extract_db_opts! ( remaining )
243+ parse_creds_hosts! ( creds_hosts )
244+ parse_db_opts! ( db_opts )
245+ end
246+
247+ def extract_db_opts! ( string )
248+ db_opts , d , creds_hosts = string . reverse . partition ( DATABASE_DELIM )
249+ db_opts , creds_hosts = creds_hosts , db_opts if creds_hosts . empty?
250+ if db_opts . empty? && creds_hosts . include? ( URI_OPTS_DELIM )
251+ raise_invalid_error! ( INVALID_OPTS_DELIM )
252+ end
253+ [ creds_hosts , db_opts ] . map { |s | s . reverse }
254+ end
255+
256+ def parse_creds_hosts! ( string )
257+ hosts , creds = split_creds_hosts ( string )
258+ @servers = parse_servers! ( hosts )
259+ @user = parse_user! ( creds )
260+ @password = parse_password! ( creds )
261+ end
262+
263+ def split_creds_hosts ( string )
264+ hosts , d , creds = string . reverse . partition ( AUTH_DELIM )
265+ hosts , creds = creds , hosts if hosts . empty?
266+ [ hosts , creds ] . map { |s | s . reverse }
267+ end
268+
269+ def parse_db_opts! ( string )
270+ auth_db , d , uri_opts = string . partition ( URI_OPTS_DELIM )
271+ @uri_options = parse_uri_options! ( uri_opts )
272+ @database = parse_database! ( auth_db )
273+ end
274+
275+ def parse_uri_options! ( string )
276+ return { } unless string
277+ string . split ( INDIV_URI_OPTS_DELIM ) . reduce ( { } ) do |uri_options , opt |
229278 raise_invalid_error! ( INVALID_OPTS_VALUE_DELIM ) unless opt . index ( URI_OPTS_VALUE_DELIM )
230279 key , value = opt . split ( URI_OPTS_VALUE_DELIM )
231280 strategy = URI_OPTION_MAP [ key . downcase ]
@@ -238,52 +287,23 @@ def parse_uri_options!(part, remaining)
238287 end
239288 end
240289
241- def extract_uri_options! ( remaining )
242- if index = remaining . index ( URI_OPTS_DELIM )
243- part = remaining [ index +1 ..-1 ]
244- remaining = remaining [ 0 ...index ]
245- end
246- [ parse_uri_options! ( part , remaining ) , remaining ]
247- end
248-
249- def parse_user! ( part )
250- if ( part && user = part . partition ( AUTH_USER_PWD_DELIM ) [ 0 ] )
290+ def parse_user! ( string )
291+ if ( string && user = string . partition ( AUTH_USER_PWD_DELIM ) [ 0 ] )
251292 raise_invalid_error! ( UNESCAPED_USER_PWD ) if user =~ UNSAFE
252- :: URI . decode ( user )
293+ decode ( user ) if user . length > 0
253294 end
254295 end
255296
256- def parse_password! ( part )
257- if ( part && pwd = part . partition ( AUTH_USER_PWD_DELIM ) [ 2 ] )
297+ def parse_password! ( string )
298+ if ( string && pwd = string . partition ( AUTH_USER_PWD_DELIM ) [ 2 ] )
258299 raise_invalid_error! ( UNESCAPED_USER_PWD ) if pwd =~ UNSAFE
259- :: URI . decode ( pwd ) unless pwd . length == 0
300+ decode ( pwd ) if pwd . length > 0
260301 end
261302 end
262303
263- def extract_auth! ( remaining )
264- if index = remaining . reverse . index ( AUTH_DELIM )
265- part = remaining [ 0 ...-( index +1 ) ]
266- remaining = remaining [ part . size +1 ..-1 ]
267- end
268- [ parse_user! ( part ) , parse_password! ( part ) , remaining ]
269- end
270-
271- def extract_database! ( remaining )
272- if index = remaining . reverse . index ( DATABSE_DELIM )
273- if index == 0
274- part = nil
275- remaining = remaining [ 0 ...-1 ]
276- else
277- db = remaining [ -index ..-1 ]
278- unless db . end_with? ( '.sock' )
279- part = db
280- remaining = remaining [ 0 ..-( part . size +2 ) ]
281- end
282- end
283- elsif !@uri_options . empty?
284- raise_invalid_error! ( INVALID_DB_DELIM )
285- end
286- [ part , remaining ]
304+ def parse_database! ( string )
305+ raise_invalid_error! ( UNESCAPED_DATABASE ) if string =~ UNSAFE
306+ decode ( string ) if string . length > 0
287307 end
288308
289309 def validate_port_string! ( port )
@@ -292,37 +312,31 @@ def validate_port_string!(port)
292312 end
293313 end
294314
295- def parse_servers! ( remaining )
296- raise_invalid_error! ( INVALID_HOST ) unless remaining . size > 0
297- remaining . split ( HOST_DELIM ) . reduce ( [ ] ) do |servers , host |
315+ def parse_servers! ( string )
316+ raise_invalid_error! ( INVALID_HOST ) unless string . size > 0
317+ string . split ( HOST_DELIM ) . reduce ( [ ] ) do |servers , host |
298318 if host [ 0 ] == '['
299319 if host . index ( ']:' )
300320 h , p = host . split ( ']:' )
301321 validate_port_string! ( p )
302322 end
303323 elsif host . index ( HOST_PORT_DELIM )
304- raise_invalid_error! ( INVALID_HOST_PORT ) unless host . count ( HOST_PORT_DELIM ) == 1
305- h , p = host . split ( HOST_PORT_DELIM )
306- raise_invalid_error! ( INVALID_HOST ) unless h
324+ h , d , p = host . partition ( HOST_PORT_DELIM )
325+ raise_invalid_error! ( INVALID_HOST ) unless h . size > 0
307326 validate_port_string! ( p )
327+ elsif host =~ UNIX_SOCKET
328+ raise_invalid_error! ( UNESCAPED_UNIX_SOCKET ) if host =~ UNSAFE
308329 end
309330 servers << host
310331 end
311332 end
312333
313- def extract_servers! ( remaining )
314- [ parse_servers! ( remaining ) , remaining ]
315- end
316-
317334 def raise_invalid_error! ( details )
318335 raise Error ::InvalidURI . new ( @string , details )
319336 end
320337
321- def setup! ( remaining )
322- @uri_options , remaining = extract_uri_options! ( remaining )
323- @user , @password , remaining = extract_auth! ( remaining ) if remaining
324- @database , remaining = extract_database! ( remaining ) if remaining
325- @servers , remaining = extract_servers! ( remaining ) if remaining
338+ def decode ( value )
339+ ::URI . decode ( value )
326340 end
327341
328342 # Hash for storing map of URI option parameters to conversion strategies
@@ -373,7 +387,7 @@ def self.uri_option(uri_key, name, extra = {})
373387 uri_option 'authsource' , :source , :group => :auth , :type => :auth_source
374388 uri_option 'authmechanism' , :auth_mech , :type => :auth_mech
375389 uri_option 'authmechanismproperties' , :auth_mech_properties , :group => :auth ,
376- :type => :auth_mech_props
390+ :type => :auth_mech_props
377391
378392 # Casts option values that do not have a specifically provided
379393 # transofrmation to the appropriate type.
@@ -389,7 +403,7 @@ def cast(value)
389403 elsif value =~ /[\d ]/
390404 value . to_i
391405 else
392- value . to_sym
406+ decode ( value ) . to_sym
393407 end
394408 end
395409
@@ -433,7 +447,11 @@ def select_target(uri_options, group = nil)
433447 # @param name [Symbol] The name of the option.
434448 def merge_uri_option ( target , value , name )
435449 if target . key? ( name )
436- target [ name ] += value
450+ if REPEATABLE_OPTIONS . include? ( name )
451+ target [ name ] += value
452+ else
453+ log_warn ( "Repeated option key: #{ name } ." )
454+ end
437455 else
438456 target . merge! ( name => value )
439457 end
@@ -460,7 +478,7 @@ def add_uri_option(strategy, value, uri_options)
460478 #
461479 # @return [String] Same value to avoid cast to Symbol.
462480 def replica_set ( value )
463- :: URI . decode ( value )
481+ decode ( value )
464482 end
465483
466484 # Auth source transformation, either db string or :external.
@@ -470,7 +488,7 @@ def replica_set(value)
470488 # @return [String] If auth source is database name.
471489 # @return [:external] If auth source is external authentication.
472490 def auth_source ( value )
473- value == '$external' ? :external : value
491+ value == '$external' ? :external : decode ( value )
474492 end
475493
476494 # Authentication mechanism transformation.
@@ -544,7 +562,7 @@ def ms_convert(value)
544562 def hash_extractor ( value )
545563 value . split ( ',' ) . reduce ( { } ) do |set , tag |
546564 k , v = tag . split ( ':' )
547- set . merge ( :: URI . decode ( k ) . downcase . to_sym => :: URI . decode ( v ) )
565+ set . merge ( decode ( k ) . downcase . to_sym => decode ( v ) )
548566 end
549567 end
550568 end
0 commit comments