@@ -1317,6 +1317,34 @@ impl ClientOptions {
13171317 }
13181318}
13191319
1320+ /// Splits the string once on the first instance of the given delimiter. If the delimiter is not
1321+ /// present, returns the entire string as the "left" side.
1322+ ///
1323+ /// e.g.
1324+ /// "abc.def" split on "." -> ("abc", Some("def"))
1325+ /// "ab.cd.ef" split on "." -> ("ab", Some("cd.ef"))
1326+ /// "abcdef" split on "." -> ("abcdef", None)
1327+ fn split_once_left < ' a > ( s : & ' a str , delimiter : & str ) -> ( & ' a str , Option < & ' a str > ) {
1328+ match s. split_once ( delimiter) {
1329+ Some ( ( l, r) ) => ( l, Some ( r) ) ,
1330+ None => ( s, None ) ,
1331+ }
1332+ }
1333+
1334+ /// Splits the string once on the last instance of the given delimiter. If the delimiter is not
1335+ /// present, returns the entire string as the "right" side.
1336+ ///
1337+ /// e.g.
1338+ /// "abd.def" split on "." -> (Some("abc"), "def")
1339+ /// "ab.cd.ef" split on "." -> (Some("ab.cd"), "ef")
1340+ /// "abcdef" split on "." -> (None, "abcdef")
1341+ fn split_once_right < ' a > ( s : & ' a str , delimiter : & str ) -> ( Option < & ' a str > , & ' a str ) {
1342+ match s. rsplit_once ( delimiter) {
1343+ Some ( ( l, r) ) => ( Some ( l) , r) ,
1344+ None => ( None , s) ,
1345+ }
1346+ }
1347+
13201348/// Splits a string into a section before a given index and a section exclusively after the index.
13211349/// Empty portions are returned as `None`.
13221350fn exclusive_split_at ( s : & str , i : usize ) -> ( Option < & str > , Option < & str > ) {
@@ -1338,12 +1366,12 @@ fn percent_decode(s: &str, err_message: &str) -> Result<String> {
13381366 }
13391367}
13401368
1341- fn validate_userinfo ( s : & str , userinfo_type : & str ) -> Result < ( ) > {
1369+ fn validate_and_parse_userinfo ( s : & str , userinfo_type : & str ) -> Result < String > {
13421370 if s. chars ( ) . any ( |c| USERINFO_RESERVED_CHARACTERS . contains ( & c) ) {
1343- return Err ( ErrorKind :: InvalidArgument {
1344- message : format ! ( "{} must be URL encoded" , userinfo_type ) ,
1345- }
1346- . into ( ) ) ;
1371+ return Err ( Error :: invalid_argument ( format ! (
1372+ "{} must be URL encoded" ,
1373+ userinfo_type
1374+ ) ) ) ;
13471375 }
13481376
13491377 // All instances of '%' in the username must be part of an percent-encoded substring. This means
@@ -1352,13 +1380,13 @@ fn validate_userinfo(s: &str, userinfo_type: &str) -> Result<()> {
13521380 . skip ( 1 )
13531381 . any ( |part| part. len ( ) < 2 || part[ 0 ..2 ] . chars ( ) . any ( |c| !c. is_ascii_hexdigit ( ) ) )
13541382 {
1355- return Err ( ErrorKind :: InvalidArgument {
1356- message : "username/password cannot contain unescaped %". to_string ( ) ,
1357- }
1358- . into ( ) ) ;
1383+ return Err ( Error :: invalid_argument ( format ! (
1384+ "{} cannot contain unescaped %",
1385+ userinfo_type
1386+ ) ) ) ;
13591387 }
13601388
1361- Ok ( ( ) )
1389+ percent_decode ( s , & format ! ( "{} must be URL encoded" , userinfo_type ) )
13621390}
13631391
13641392impl TryFrom < & str > for ConnectionString {
@@ -1390,116 +1418,60 @@ impl ConnectionString {
13901418 /// malformed or one of the options has an invalid value, an error will be returned.
13911419 pub fn parse ( s : impl AsRef < str > ) -> Result < Self > {
13921420 let s = s. as_ref ( ) ;
1393- let end_of_scheme = match s. find ( "://" ) {
1394- Some ( index) => index,
1395- None => {
1396- return Err ( ErrorKind :: InvalidArgument {
1397- message : "connection string contains no scheme" . to_string ( ) ,
1398- }
1399- . into ( ) )
1400- }
1421+
1422+ let Some ( ( scheme, after_scheme) ) = s. split_once ( "://" ) else {
1423+ return Err ( Error :: invalid_argument (
1424+ "connection string contains no scheme" ,
1425+ ) ) ;
14011426 } ;
14021427
1403- let srv = match & s [ ..end_of_scheme ] {
1428+ let srv = match scheme {
14041429 "mongodb" => false ,
1430+ #[ cfg( feature = "dns-resolver" ) ]
14051431 "mongodb+srv" => true ,
1406- _ => {
1407- return Err ( ErrorKind :: InvalidArgument {
1408- message : format ! ( "invalid connection string scheme: {}" , & s[ ..end_of_scheme] ) ,
1409- }
1410- . into ( ) )
1432+ #[ cfg( not( feature = "dns-resolver" ) ) ]
1433+ "mongodb+srv" => {
1434+ return Err ( Error :: invalid_argument (
1435+ "mongodb+srv connection strings cannot be used when the 'dns-resolver' \
1436+ feature is disabled",
1437+ ) )
14111438 }
1412- } ;
1413- #[ cfg( not( feature = "dns-resolver" ) ) ]
1414- if srv {
1415- return Err ( Error :: invalid_argument (
1416- "mongodb+srv connection strings cannot be used when the 'dns-resolver' feature is \
1417- disabled",
1418- ) ) ;
1419- }
1420-
1421- let after_scheme = & s[ end_of_scheme + 3 ..] ;
1422-
1423- let ( pre_slash, post_slash) = match after_scheme. find ( '/' ) {
1424- Some ( slash_index) => match exclusive_split_at ( after_scheme, slash_index) {
1425- ( Some ( section) , o) => ( section, o) ,
1426- ( None , _) => {
1427- return Err ( ErrorKind :: InvalidArgument {
1428- message : "missing hosts" . to_string ( ) ,
1429- }
1430- . into ( ) )
1431- }
1432- } ,
1433- None => {
1434- if after_scheme. find ( '?' ) . is_some ( ) {
1435- return Err ( ErrorKind :: InvalidArgument {
1436- message : "Missing delimiting slash between hosts and options" . to_string ( ) ,
1437- }
1438- . into ( ) ) ;
1439- }
1440- ( after_scheme, None )
1439+ other => {
1440+ return Err ( Error :: invalid_argument ( format ! (
1441+ "unsupported connection string scheme: {}" ,
1442+ other
1443+ ) ) )
14411444 }
14421445 } ;
14431446
1444- let ( database, options_section) = match post_slash {
1445- Some ( section) => match section. find ( '?' ) {
1446- Some ( index) => exclusive_split_at ( section, index) ,
1447- None => ( post_slash, None ) ,
1448- } ,
1449- None => ( None , None ) ,
1450- } ;
1451-
1452- let db = match database {
1453- Some ( db) => {
1454- let decoded = percent_decode ( db, "database name must be URL encoded" ) ?;
1455- if decoded
1456- . chars ( )
1457- . any ( |c| ILLEGAL_DATABASE_CHARACTERS . contains ( & c) )
1458- {
1459- return Err ( ErrorKind :: InvalidArgument {
1460- message : "illegal character in database name" . to_string ( ) ,
1461- }
1462- . into ( ) ) ;
1463- }
1464- Some ( decoded)
1465- }
1466- None => None ,
1467- } ;
1447+ let ( pre_options, options) = split_once_left ( after_scheme, "?" ) ;
1448+ let ( user_info, hosts_and_auth_db) = split_once_right ( pre_options, "@" ) ;
14681449
1469- let ( authentication_requested, cred_section, hosts_section) = match pre_slash. rfind ( '@' ) {
1470- Some ( index) => {
1471- // if '@' is in the host section, it MUST be interpreted as a request for
1472- // authentication, even if the credentials are empty.
1473- let ( creds, hosts) = exclusive_split_at ( pre_slash, index) ;
1474- match hosts {
1475- Some ( hs) => ( true , creds, hs) ,
1476- None => {
1477- return Err ( ErrorKind :: InvalidArgument {
1478- message : "missing hosts" . to_string ( ) ,
1479- }
1480- . into ( ) )
1481- }
1482- }
1450+ // if '@' is in the host section, it MUST be interpreted as a request for authentication
1451+ let authentication_requested = user_info. is_some ( ) ;
1452+ let ( username, password) = match user_info {
1453+ Some ( user_info) => {
1454+ let ( username, password) = split_once_left ( user_info, ":" ) ;
1455+ let username = if username. is_empty ( ) {
1456+ None
1457+ } else {
1458+ Some ( validate_and_parse_userinfo ( username, "username" ) ?)
1459+ } ;
1460+ let password = match password {
1461+ Some ( password) => Some ( validate_and_parse_userinfo ( password, "password" ) ?) ,
1462+ None => None ,
1463+ } ;
1464+ ( username, password)
14831465 }
1484- None => ( false , None , pre_slash) ,
1485- } ;
1486-
1487- let ( username, password) = match cred_section {
1488- Some ( creds) => match creds. find ( ':' ) {
1489- Some ( index) => match exclusive_split_at ( creds, index) {
1490- ( username, None ) => ( username, Some ( "" ) ) ,
1491- ( username, password) => ( username, password) ,
1492- } ,
1493- None => ( Some ( creds) , None ) , // Lack of ":" implies whole string is username
1494- } ,
14951466 None => ( None , None ) ,
14961467 } ;
14971468
1498- let hosts = hosts_section
1499- . split ( ',' )
1469+ let ( hosts, auth_db) = split_once_left ( hosts_and_auth_db, "/" ) ;
1470+
1471+ let hosts = hosts
1472+ . split ( "," )
15001473 . map ( ServerAddress :: parse)
15011474 . collect :: < Result < Vec < ServerAddress > > > ( ) ?;
1502-
15031475 let host_info = if !srv {
15041476 HostInfo :: HostIdentifiers ( hosts)
15051477 } else {
@@ -1527,17 +1499,32 @@ impl ConnectionString {
15271499 }
15281500 } ;
15291501
1502+ let db = match auth_db {
1503+ Some ( "" ) | None => None ,
1504+ Some ( db) => {
1505+ let decoded = percent_decode ( db, "database name must be URL encoded" ) ?;
1506+ for c in decoded. chars ( ) {
1507+ if ILLEGAL_DATABASE_CHARACTERS . contains ( & c) {
1508+ return Err ( Error :: invalid_argument ( format ! (
1509+ "illegal character in database name: {}" ,
1510+ c
1511+ ) ) ) ;
1512+ }
1513+ }
1514+ Some ( decoded)
1515+ }
1516+ } ;
1517+
15301518 let mut conn_str = ConnectionString {
15311519 host_info,
15321520 #[ cfg( test) ]
15331521 original_uri : s. into ( ) ,
15341522 ..Default :: default ( )
15351523 } ;
15361524
1537- let mut parts = if let Some ( opts) = options_section {
1538- conn_str. parse_options ( opts) ?
1539- } else {
1540- ConnectionStringParts :: default ( )
1525+ let mut parts = match options {
1526+ Some ( options) => conn_str. parse_options ( options) ?,
1527+ None => ConnectionStringParts :: default ( ) ,
15411528 } ;
15421529
15431530 if conn_str. srv_service_name . is_some ( ) && !srv {
@@ -1566,19 +1553,10 @@ impl ConnectionString {
15661553 }
15671554 }
15681555
1569- // Set username and password.
1570- if let Some ( u) = username {
1556+ if let Some ( username) = username {
15711557 let credential = conn_str. credential . get_or_insert_with ( Default :: default) ;
1572- validate_userinfo ( u, "username" ) ?;
1573- let decoded_u = percent_decode ( u, "username must be URL encoded" ) ?;
1574-
1575- credential. username = Some ( decoded_u) ;
1576-
1577- if let Some ( pass) = password {
1578- validate_userinfo ( pass, "password" ) ?;
1579- let decoded_p = percent_decode ( pass, "password must be URL encoded" ) ?;
1580- credential. password = Some ( decoded_p)
1581- }
1558+ credential. username = Some ( username) ;
1559+ credential. password = password;
15821560 }
15831561
15841562 if parts. auth_source . as_deref ( ) == Some ( "" ) {
0 commit comments