sqlx_core_oldapi/mssql/options/
parse.rs

1use crate::error::Error;
2use crate::mssql::protocol::pre_login::Encrypt;
3use crate::mssql::MssqlConnectOptions;
4use percent_encoding::percent_decode_str;
5use std::str::FromStr;
6use url::Url;
7
8impl FromStr for MssqlConnectOptions {
9    type Err = Error;
10
11    /// Parse a connection string into a set of connection options.
12    ///
13    /// The connection string should be a valid URL with the following format:
14    /// ```text
15    /// mssql://[username[:password]@]host[:port][/database][?param1=value1&param2=value2...]
16    /// ```
17    ///
18    /// Components:
19    /// - `username`: The username for SQL Server authentication.
20    /// - `password`: The password for SQL Server authentication.
21    /// - `host`: The hostname or IP address of the SQL Server.
22    /// - `port`: The port number. If not specified, defaults to 1433 or is discovered via SSRP when using named instances.
23    /// - `database`: The name of the database to connect to.
24    ///
25    /// Supported query parameters:
26    /// - `instance`: SQL Server named instance. When specified without an explicit port, the port is automatically discovered using the SQL Server Resolution Protocol (SSRP). If a port is explicitly specified, SSRP is not used.
27    /// - `encrypt`: Controls connection encryption:
28    ///   - `strict`: Requires encryption and validates the server certificate.
29    ///   - `mandatory` or `true` or `yes`: Requires encryption but doesn't validate the server certificate.
30    ///   - `optional` or `false` or `no`: Uses encryption if available, falls back to unencrypted.
31    ///   - `not_supported`: No encryption.
32    /// - `sslrootcert` or `ssl-root-cert` or `ssl-ca`: Path to the root certificate for validating the server's SSL certificate.
33    /// - `trust_server_certificate`: When true, skips validation of the server's SSL certificate. Use with caution as it makes the connection vulnerable to man-in-the-middle attacks.
34    /// - `hostname_in_certificate`: The hostname expected in the server's SSL certificate. Use this when the server's hostname doesn't match the certificate.
35    /// - `packet_size`: Size of TDS packets in bytes. Larger sizes can improve performance but consume more memory on the server
36    /// - `client_program_version`: Version number of the client program, sent to the server for logging purposes.
37    /// - `client_pid`: Process ID of the client, sent to the server for logging purposes.
38    /// - `hostname`: Name of the client machine, sent to the server for logging purposes.
39    /// - `app_name`: Name of the client application, sent to the server for logging purposes.
40    /// - `server_name`: Name of the server to connect to. Useful when connecting through a proxy or load balancer.
41    /// - `client_interface_name`: Name of the client interface, sent to the server for logging purposes.
42    /// - `language`: Sets the language for server messages. Affects date formats and system messages.
43    ///
44    /// Examples:
45    /// ```text
46    /// mssql://user:pass@localhost:1433/mydb?encrypt=strict&app_name=MyApp&packet_size=4096
47    /// mssql://user:pass@localhost/mydb?instance=SQLEXPRESS
48    /// ```
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        let url: Url = s.parse().map_err(Error::config)?;
51        let mut options = Self::new();
52
53        if let Some(host) = url.host_str() {
54            options = options.host(host);
55        }
56
57        if let Some(port) = url.port() {
58            options = options.port(port);
59        }
60
61        let username = url.username();
62        if !username.is_empty() {
63            options = options.username(
64                &percent_decode_str(username)
65                    .decode_utf8()
66                    .map_err(Error::config)?,
67            );
68        }
69
70        if let Some(password) = url.password() {
71            options = options.password(
72                &percent_decode_str(password)
73                    .decode_utf8()
74                    .map_err(Error::config)?,
75            );
76        }
77
78        let path = url.path().trim_start_matches('/');
79        if !path.is_empty() {
80            options = options.database(path);
81        }
82
83        for (key, value) in url.query_pairs() {
84            match key.as_ref() {
85                "instance" => {
86                    options = options.instance(&value);
87                }
88                "encrypt" => {
89                    match value.to_lowercase().as_str() {
90                        "strict" => options = options.encrypt(Encrypt::Required),
91                        "mandatory" | "true" | "yes" => options = options.encrypt(Encrypt::On),
92                        "optional" | "false" | "no" => options = options.encrypt(Encrypt::Off),
93                        "not_supported" => options = options.encrypt(Encrypt::NotSupported),
94                        _ => return Err(Error::config(MssqlInvalidOption(format!(
95                            "encrypt={} is not a valid value for encrypt. Valid values are: strict, mandatory, optional, true, false, yes, no",
96                            value
97                        )))),
98                    }
99                }
100                "sslrootcert" | "ssl-root-cert" | "ssl-ca" => {
101                    options = options.ssl_root_cert(&*value);
102                }
103                "trust_server_certificate" => {
104                    let trust = value.parse::<bool>().map_err(Error::config)?;
105                    options = options.trust_server_certificate(trust);
106                }
107                "hostname_in_certificate" => {
108                    options = options.hostname_in_certificate(&value);
109                }
110                "packet_size" => {
111                    let size = value.parse().map_err(Error::config)?;
112                    options = options.requested_packet_size(size).map_err(|_| {
113                        Error::config(MssqlInvalidOption(format!("packet_size={}", size)))
114                    })?;
115                }
116                "client_program_version" => {
117                    options = options.client_program_version(value.parse().map_err(Error::config)?)
118                }
119                "client_pid" => options = options.client_pid(value.parse().map_err(Error::config)?),
120                "hostname" => options = options.hostname(&value),
121                "app_name" => options = options.app_name(&value),
122                "server_name" => options = options.server_name(&value),
123                "client_interface_name" => options = options.client_interface_name(&value),
124                "language" => options = options.language(&value),
125                _ => {
126                    return Err(Error::config(MssqlInvalidOption(key.into())));
127                }
128            }
129        }
130        Ok(options)
131    }
132}
133
134#[derive(Debug)]
135struct MssqlInvalidOption(String);
136
137impl std::fmt::Display for MssqlInvalidOption {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(f, "`{}` is not a valid mssql connection option", self.0)
140    }
141}
142
143impl std::error::Error for MssqlInvalidOption {}
144
145#[test]
146fn it_parses_username_with_at_sign_correctly() {
147    let url = "mssql://user@hostname:password@hostname:5432/database";
148    let opts = MssqlConnectOptions::from_str(url).unwrap();
149
150    assert_eq!("user@hostname", &opts.username);
151}
152
153#[test]
154fn it_parses_password_with_non_ascii_chars_correctly() {
155    let url = "mssql://username:p@ssw0rd@hostname:5432/database";
156    let opts = MssqlConnectOptions::from_str(url).unwrap();
157
158    assert_eq!(Some("p@ssw0rd".into()), opts.password);
159}