@@ -4,10 +4,20 @@ import SwiftBSON
44/// Represents a MongoDB connection string.
55/// - SeeAlso: https://docs.mongodb.com/manual/reference/connection-string/
66public struct MongoConnectionString : Codable , LosslessStringConvertible {
7+ /// Characters that must not be present in a database name.
78 private static let forbiddenDBCharacters = [ " / " , " \\ " , " " , " \" " , " $ " ]
8- /// Forbidden characters per RFC 3986.
9+
10+ /// General delimiters as defined by RFC 3986. These characters must be percent-encoded when present in the hosts,
11+ /// default authentication database, and user info.
912 /// - SeeAlso: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
10- fileprivate static let forbiddenUserInfoCharacters = [ " : " , " / " , " ? " , " # " , " [ " , " ] " , " @ " ]
13+ fileprivate static let genDelims = " :/?#[]@ "
14+
15+ /// Characters that do not need to be percent-encoded when reconstructing the hosts, default authentication
16+ /// database, and user info.
17+ fileprivate static let allowedForNonOptionEncoding = CharacterSet ( charactersIn: genDelims) . inverted
18+
19+ /// Characters that do not need to be percent-encoded when reconstructing URI options.
20+ fileprivate static let allowedForOptionEncoding = CharacterSet ( charactersIn: " =&,: " ) . inverted
1121
1222 fileprivate enum OptionName : String {
1323 case appName = " appname "
@@ -161,17 +171,21 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
161171 }
162172
163173 public var description : String {
164- var ret = " "
174+ var hostDescription = " "
165175 switch self . type {
166176 case . ipLiteral:
167- ret += " [ \( self . host) ] "
168- default :
169- ret += " \( self . host) "
177+ hostDescription += " [ \( self . host) ] "
178+ case . ipv4:
179+ hostDescription += self . host
180+ case . unixDomainSocket, . hostname:
181+ hostDescription += self . host. getPercentEncoded (
182+ withAllowedCharacters: MongoConnectionString . allowedForNonOptionEncoding
183+ )
170184 }
171185 if let port = self . port {
172- ret += " : \( port) "
186+ hostDescription += " : \( port) "
173187 }
174- return ret
188+ return hostDescription
175189 }
176190 }
177191
@@ -573,7 +587,7 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
573587 self . credential? . source = authSource
574588 } else {
575589 // The authentication mechanism defaults to SCRAM if an authMechanism is not provided, and SCRAM
576- // requires a username
590+ // requires a username.
577591 throw MongoError . InvalidArgumentError (
578592 message: " No username provided for authentication in the connection string but an authentication "
579593 + " source was provided. To use an authentication mechanism that does not require a username, "
@@ -754,12 +768,104 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
754768 try ? self . init ( throwsIfInvalid: description)
755769 }
756770
757- // TODO: SWIFT-1405: add options to description
758771 public var description : String {
759- var des = " "
760- des += " \( self . scheme) :// "
761- des += self . hosts. map { $0. description } . joined ( separator: " , " )
762- return des
772+ var uri = " "
773+ uri += " \( self . scheme) :// "
774+ if let username = self . credential? . username {
775+ uri += username. getPercentEncoded ( withAllowedCharacters: Self . allowedForNonOptionEncoding)
776+ if let password = self . credential? . password {
777+ uri += " : " + password. getPercentEncoded ( withAllowedCharacters: Self . allowedForNonOptionEncoding)
778+ }
779+ uri += " @ "
780+ }
781+ uri += self . hosts. map { $0. description } . joined ( separator: " , " )
782+ // A trailing slash in the connection string is valid so we can append this unconditionally.
783+ uri += " / "
784+ if let defaultAuthDB = self . defaultAuthDB {
785+ uri += defaultAuthDB. getPercentEncoded ( withAllowedCharacters: Self . allowedForNonOptionEncoding)
786+ }
787+ uri += " ? "
788+ uri. appendOption ( name: . appName, option: self . appName)
789+ uri. appendOption ( name: . authMechanism, option: self . credential? . mechanism? . description)
790+ uri. appendOption ( name: . authMechanismProperties, option: self . credential? . mechanismProperties? . map {
791+ var property = $0. key + " : "
792+ switch $0. value {
793+ case let . string( s) :
794+ property += s
795+ case let . bool( b) :
796+ property += String ( b)
797+ // the possible values for authMechanismProperties are only strings and booleans
798+ default :
799+ property += " "
800+ }
801+ return property
802+ } . joined ( separator: " , " ) )
803+ uri. appendOption ( name: . authSource, option: self . credential? . source)
804+ uri. appendOption ( name: . compressors, option: self . compressors? . map {
805+ switch $0. _compressor {
806+ case let . zlib( level) :
807+ uri. appendOption ( name: . zlibCompressionLevel, option: level)
808+ return " zlib "
809+ }
810+ } . joined ( separator: " , " ) )
811+ uri. appendOption ( name: . connectTimeoutMS, option: self . connectTimeoutMS)
812+ uri. appendOption ( name: . directConnection, option: self . directConnection)
813+ uri. appendOption ( name: . heartbeatFrequencyMS, option: self . heartbeatFrequencyMS)
814+ uri. appendOption ( name: . journal, option: self . writeConcern? . journal)
815+ uri. appendOption ( name: . loadBalanced, option: self . loadBalanced)
816+ uri. appendOption ( name: . localThresholdMS, option: self . localThresholdMS)
817+ uri. appendOption ( name: . maxPoolSize, option: self . maxPoolSize)
818+ uri. appendOption ( name: . maxStalenessSeconds, option: self . readPreference? . maxStalenessSeconds)
819+ uri. appendOption ( name: . readConcernLevel, option: self . readConcern? . level)
820+ uri. appendOption ( name: . readPreference, option: self . readPreference? . mode. rawValue)
821+ if let tagSets = self . readPreference? . tagSets {
822+ for tags in tagSets {
823+ uri. appendOption ( name: . readPreferenceTags, option: tags. map {
824+ var tag = $0. key + " : "
825+ switch $0. value {
826+ case let . string( s) :
827+ tag += s
828+ // tags are always parsed as strings
829+ default :
830+ tag += " "
831+ }
832+ return tag
833+ } . joined ( separator: " , " ) )
834+ }
835+ }
836+ uri. appendOption ( name: . replicaSet, option: self . replicaSet)
837+ uri. appendOption ( name: . retryReads, option: self . retryReads)
838+ uri. appendOption ( name: . retryWrites, option: self . retryWrites)
839+ uri. appendOption ( name: . serverSelectionTimeoutMS, option: self . serverSelectionTimeoutMS)
840+ uri. appendOption ( name: . socketTimeoutMS, option: self . socketTimeoutMS)
841+ uri. appendOption ( name: . srvMaxHosts, option: self . srvMaxHosts)
842+ uri. appendOption ( name: . srvServiceName, option: self . srvServiceName)
843+ uri. appendOption ( name: . tls, option: self . tls)
844+ uri. appendOption ( name: . tlsAllowInvalidCertificates, option: self . tlsAllowInvalidCertificates)
845+ uri. appendOption ( name: . tlsAllowInvalidHostnames, option: self . tlsAllowInvalidHostnames)
846+ uri. appendOption ( name: . tlsCAFile, option: self . tlsCAFile)
847+ uri. appendOption ( name: . tlsCertificateKeyFile, option: self . tlsCertificateKeyFile)
848+ uri. appendOption ( name: . tlsCertificateKeyFilePassword, option: self . tlsCertificateKeyFilePassword)
849+ uri. appendOption (
850+ name: . tlsDisableCertificateRevocationCheck,
851+ option: self . tlsDisableCertificateRevocationCheck
852+ )
853+ uri. appendOption ( name: . tlsDisableOCSPEndpointCheck, option: self . tlsDisableOCSPEndpointCheck)
854+ uri. appendOption ( name: . tlsInsecure, option: self . tlsInsecure)
855+ uri. appendOption ( name: . w, option: self . writeConcern? . w. map {
856+ switch $0 {
857+ case let . number( n) :
858+ return String ( n)
859+ case . majority:
860+ return " majority "
861+ case let . custom( other) :
862+ return other
863+ }
864+ } )
865+ uri. appendOption ( name: . wTimeoutMS, option: self . writeConcern? . wtimeoutMS)
866+ // Pop either the trailing "&" or the trailing "?" if no options were present.
867+ _ = uri. popLast ( )
868+ return uri
763869 }
764870
765871 /// Returns a document containing all of the options provided after the ? of the URI.
@@ -769,15 +875,15 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
769875 if let appName = self . appName {
770876 options [ . appName] = . string( appName)
771877 }
772- if let authSource = self . credential? . source {
773- options [ . authSource] = . string( authSource)
774- }
775878 if let authMechanism = self . credential? . mechanism {
776879 options [ . authMechanism] = . string( authMechanism. description)
777880 }
778881 if let authMechanismProperties = self . credential? . mechanismProperties {
779882 options [ . authMechanismProperties] = . document( authMechanismProperties)
780883 }
884+ if let authSource = self . credential? . source {
885+ options [ . authSource] = . string( authSource)
886+ }
781887 if let compressors = self . compressors {
782888 var compressorNames : [ BSON ] = [ ]
783889 for compressor in compressors {
@@ -1035,6 +1141,17 @@ extension Compressor {
10351141 }
10361142}
10371143
1144+ extension String {
1145+ fileprivate mutating func appendOption( name: MongoConnectionString . OptionName , option: CustomStringConvertible ? ) {
1146+ if let option = option {
1147+ let optionString = option. description. getPercentEncoded (
1148+ withAllowedCharacters: MongoConnectionString . allowedForOptionEncoding
1149+ )
1150+ self += name. rawValue + " = " + optionString + " & "
1151+ }
1152+ }
1153+ }
1154+
10381155extension StringProtocol {
10391156 fileprivate func getPercentDecoded( forKey key: String ) throws -> String {
10401157 guard let decoded = self . removingPercentEncoding else {
@@ -1058,8 +1175,12 @@ extension StringProtocol {
10581175 return true
10591176 }
10601177
1178+ fileprivate func getPercentEncoded( withAllowedCharacters allowed: CharacterSet ) -> String {
1179+ self . addingPercentEncoding ( withAllowedCharacters: allowed) ?? String ( self )
1180+ }
1181+
10611182 fileprivate func getValidatedUserInfo( forKey key: String ) throws -> String {
1062- for character in MongoConnectionString . forbiddenUserInfoCharacters {
1183+ for character in MongoConnectionString . genDelims {
10631184 if self . contains ( character) {
10641185 throw MongoError . InvalidArgumentError (
10651186 message: " \( key) in the connection string contains invalid character that must be percent-encoded: "
0 commit comments