@@ -120,10 +120,6 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
120120
121121 /// Initializes a ServerAddress, using the default localhost:27017 if a host/port is not provided.
122122 internal init ( _ hostAndPort: String = " localhost:27017 " ) throws {
123- guard !hostAndPort. contains ( " ? " ) else {
124- throw MongoError . InvalidArgumentError ( message: " \( hostAndPort) contains invalid characters " )
125- }
126-
127123 // Check if host is an IPv6 literal.
128124 if hostAndPort. first == " [ " {
129125 let ipLiteralRegex = try NSRegularExpression ( pattern: #"^\[(.*)\](?::([0-9]+))?$"# )
@@ -228,7 +224,7 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
228224 fileprivate var wTimeoutMS : Int ?
229225 fileprivate var zlibCompressionLevel : Int ?
230226
231- fileprivate init ( _ uriOptions: String ) throws {
227+ fileprivate init ( _ uriOptions: Substring ) throws {
232228 let options = uriOptions. components ( separatedBy: " & " )
233229 // tracks options that have already been set to error on duplicates
234230 var setOptions : Set < String > = [ ]
@@ -384,30 +380,51 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
384380 /// - Throws:
385381 /// - `MongoError.InvalidArgumentError` if the input is invalid.
386382 public init ( string input: String ) throws {
383+ // Parse the connection string's scheme.
387384 let schemeAndRest = input. components ( separatedBy: " :// " )
388385 guard schemeAndRest. count == 2 , let scheme = Scheme ( schemeAndRest [ 0 ] ) else {
389386 throw MongoError . InvalidArgumentError (
390387 message: " Invalid connection string scheme, expecting \' mongodb \' or \' mongodb+srv \' "
391388 )
392389 }
393390 guard !schemeAndRest[ 1 ] . isEmpty else {
394- throw MongoError . InvalidArgumentError ( message: " Invalid connection string" )
391+ throw MongoError . InvalidArgumentError ( message: " The connection string must contain host information " )
395392 }
396- let identifiersAndOptions = schemeAndRest [ 1 ] . components ( separatedBy: " / " )
397- guard identifiersAndOptions. count <= 2 else {
393+
394+ // Split the rest of the connection string into its components.
395+ let infoAndOptions = schemeAndRest [ 1 ] . split ( separator: " ? " , maxSplits: 1 , omittingEmptySubsequences: false )
396+ if infoAndOptions [ 0 ] . isEmpty {
397+ throw MongoError . InvalidArgumentError ( message: " The connection string must contain host information " )
398+ }
399+ let userHostsAndAuthDB = infoAndOptions [ 0 ] . split ( separator: " / " , omittingEmptySubsequences: false )
400+ if userHostsAndAuthDB. count > 2 {
401+ throw MongoError . InvalidArgumentError (
402+ message: " The user information, host information, and defaultAuthDB in the connection string must not "
403+ + " contain unescaped slashes "
404+ )
405+ } else if userHostsAndAuthDB. count == 1 && infoAndOptions. count == 2 {
398406 throw MongoError . InvalidArgumentError (
399- message: " Connection string contains an unescaped slash "
407+ message: " The connection string must contain a delimiting slash between the host information and "
408+ + " options "
400409 )
401410 }
402- let userAndHost = identifiersAndOptions [ 0 ] . components ( separatedBy: " @ " )
403- guard userAndHost. count <= 2 else {
404- throw MongoError . InvalidArgumentError ( message: " Connection string contains an unescaped @ symbol " )
411+ let userInfoAndHosts = userHostsAndAuthDB [ 0 ] . split ( separator: " @ " , omittingEmptySubsequences: false )
412+ if userInfoAndHosts. count > 2 {
413+ throw MongoError . InvalidArgumentError (
414+ message: " The user information and host information in the connection string must not contain "
415+ + " unescaped @ symbols "
416+ )
405417 }
406418
407- // do not omit empty subsequences to include an empty password
408- let userInfo = userAndHost. count == 2 ?
409- userAndHost [ 0 ] . split ( separator: " : " , maxSplits: 1 , omittingEmptySubsequences: false ) : nil
410- if let userInfo = userInfo {
419+ // Parse user information if present and set the hosts string.
420+ let hostsString : Substring
421+ if userInfoAndHosts. count == 2 {
422+ let userInfo = userInfoAndHosts [ 0 ] . split ( separator: " : " , omittingEmptySubsequences: false )
423+ if userInfo. count > 2 {
424+ throw MongoError . InvalidArgumentError (
425+ message: " Username and password in the connection string must not contain unescaped colons "
426+ )
427+ }
411428 var credential = MongoCredential ( )
412429 credential. username = try userInfo [ 0 ] . getValidatedUserInfo ( forKey: " username " )
413430 if userInfo. count == 2 {
@@ -416,11 +433,16 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
416433 // If no other authentication options or defaultAuthDB were provided, we should use "admin" as the
417434 // credential source. This will be overwritten later if a defaultAuthDB or an authSource is provided.
418435 credential. source = " admin "
436+ // Overwrite the sourceFromAuthSource field to false as the source is a default.
437+ credential. sourceFromAuthSource = false
419438 self . credential = credential
439+ hostsString = userInfoAndHosts [ 1 ]
440+ } else {
441+ hostsString = userInfoAndHosts [ 0 ]
420442 }
421443
422- let hostString = userInfo != nil ? userAndHost [ 1 ] : userAndHost [ 0 ]
423- let hosts = try hostString . components ( separatedBy: " , " ) . map ( HostIdentifier . init)
444+ // Parse host information.
445+ let hosts = try hostsString . components ( separatedBy: " , " ) . map ( HostIdentifier . init)
424446 if case . srv = scheme {
425447 guard hosts. count == 1 else {
426448 throw MongoError . InvalidArgumentError (
@@ -432,45 +454,44 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
432454 message: " A port cannot be specified in a mongodb+srv connection string "
433455 )
434456 }
457+ guard hosts [ 0 ] . host. filter ( { $0 == " . " } ) . count >= 2 else {
458+ throw MongoError . InvalidArgumentError (
459+ message: " The host specified in a mongodb+srv connection string must contain a host name, a domain "
460+ + " name, and a top-level domain "
461+ )
462+ }
435463 }
436464 self . scheme = scheme
437465 self . hosts = hosts
438466
439- guard identifiersAndOptions. count == 2 else {
440- // no auth DB or options were specified
441- try self . validate ( )
442- return
443- }
444-
445- let authDatabaseAndOptions = identifiersAndOptions [ 1 ] . components ( separatedBy: " ? " )
446- guard authDatabaseAndOptions. count <= 2 else {
447- throw MongoError . InvalidArgumentError ( message: " Connection string contains an unescaped question mark " )
448- }
449- if !authDatabaseAndOptions[ 0 ] . isEmpty {
450- let decoded = try authDatabaseAndOptions [ 0 ] . getPercentDecoded ( forKey: " defaultAuthDB " )
467+ // Parse the defaultAuthDB if present.
468+ if userHostsAndAuthDB. count == 2 && !userHostsAndAuthDB[ 1 ] . isEmpty {
469+ let defaultAuthDB = try userHostsAndAuthDB [ 1 ] . getPercentDecoded ( forKey: " defaultAuthDB " )
451470 for character in Self . forbiddenDBCharacters {
452- if decoded . contains ( character) {
471+ if defaultAuthDB . contains ( character) {
453472 throw MongoError . InvalidArgumentError (
454473 message: " defaultAuthDB contains invalid character: \( character) "
455474 )
456475 }
457476 }
458- self . defaultAuthDB = decoded
477+ self . defaultAuthDB = defaultAuthDB
459478 // If no other authentication options were provided, we should use the defaultAuthDB as the credential
460479 // source. This will be overwritten later if an authSource is provided.
461480 if self . credential == nil {
462481 self . credential = MongoCredential ( )
463482 }
464- self . credential? . source = decoded
483+ self . credential? . source = defaultAuthDB
484+ // Overwrite the sourceFromAuthSource field to false as the source is a default.
485+ self . credential? . sourceFromAuthSource = false
465486 }
466487
467- guard authDatabaseAndOptions . count == 2 else {
468- // no options were specified
488+ // Return early if no options were specified.
489+ guard infoAndOptions . count == 2 else {
469490 try self . validate ( )
470491 return
471492 }
472493
473- let options = try Options ( authDatabaseAndOptions [ 1 ] )
494+ let options = try Options ( infoAndOptions [ 1 ] )
474495
475496 // Validate and set compressors. This validation is only necessary for compressors provided in the URI string
476497 // and therefore is not included in the general validate method.
@@ -617,7 +638,7 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
617638 try self . validate ( )
618639 }
619640
620- private mutating func validate( ) throws {
641+ internal mutating func validate( ) throws {
621642 func optionError( name: OptionName , violation: String ) -> MongoError . InvalidArgumentError {
622643 MongoError . InvalidArgumentError (
623644 message: " Value for \( name) in the connection string must " + violation
@@ -717,6 +738,8 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
717738 ?? self . defaultAuthDB
718739 ?? " admin "
719740 self . credential? . source = defaultSource
741+ // Overwrite the sourceFromAuthSource field to false as the source is a default.
742+ self . credential? . sourceFromAuthSource = false
720743 }
721744 if self . credential? . mechanism != nil {
722745 // credential cannot be nil within the external conditional
@@ -931,7 +954,9 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
931954 }
932955 return property
933956 } . joined ( separator: " , " ) )
934- uri. appendOption ( name: . authSource, option: self . credential? . source)
957+ if self . credential? . sourceFromAuthSource == true {
958+ uri. appendOption ( name: . authSource, option: self . credential? . source)
959+ }
935960 uri. appendOption ( name: . compressors, option: self . compressors? . map {
936961 switch $0. _compressor {
937962 case let . zlib( level) :
0 commit comments