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 (default is 1433).
23    /// - `database`: The name of the database to connect to.
24    ///
25    /// Supported query parameters:
26    /// - `instance`: SQL Server named instance.
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    /// Example:
45    /// ```text
46    /// mssql://user:pass@localhost:1433/mydb?encrypt=strict&app_name=MyApp&packet_size=4096
47    /// ```
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        let url: Url = s.parse().map_err(Error::config)?;
50        let mut options = Self::new();
51
52        if let Some(host) = url.host_str() {
53            options = options.host(host);
54        }
55
56        if let Some(port) = url.port() {
57            options = options.port(port);
58        }
59
60        let username = url.username();
61        if !username.is_empty() {
62            options = options.username(
63                &*percent_decode_str(username)
64                    .decode_utf8()
65                    .map_err(Error::config)?,
66            );
67        }
68
69        if let Some(password) = url.password() {
70            options = options.password(
71                &*percent_decode_str(password)
72                    .decode_utf8()
73                    .map_err(Error::config)?,
74            );
75        }
76
77        let path = url.path().trim_start_matches('/');
78        if !path.is_empty() {
79            options = options.database(path);
80        }
81
82        for (key, value) in url.query_pairs() {
83            match key.as_ref() {
84                "instance" => {
85                    options = options.instance(&*value);
86                }
87                "encrypt" => {
88                    match value.to_lowercase().as_str() {
89                        "strict" => options = options.encrypt(Encrypt::Required),
90                        "mandatory" | "true" | "yes" => options = options.encrypt(Encrypt::On),
91                        "optional" | "false" | "no" => options = options.encrypt(Encrypt::Off),
92                        "not_supported" => options = options.encrypt(Encrypt::NotSupported),
93                        _ => return Err(Error::config(MssqlInvalidOption(format!(
94                            "encrypt={} is not a valid value for encrypt. Valid values are: strict, mandatory, optional, true, false, yes, no",
95                            value
96                        )))),
97                    }
98                }
99                "sslrootcert" | "ssl-root-cert" | "ssl-ca" => {
100                    options = options.ssl_root_cert(&*value);
101                }
102                "trust_server_certificate" => {
103                    let trust = value.parse::<bool>().map_err(Error::config)?;
104                    options = options.trust_server_certificate(trust);
105                }
106                "hostname_in_certificate" => {
107                    options = options.hostname_in_certificate(&*value);
108                }
109                "packet_size" => {
110                    let size = value.parse().map_err(Error::config)?;
111                    options = options.requested_packet_size(size).map_err(|_| {
112                        Error::config(MssqlInvalidOption(format!("packet_size={}", size)))
113                    })?;
114                }
115                "client_program_version" => {
116                    options = options.client_program_version(value.parse().map_err(Error::config)?)
117                }
118                "client_pid" => options = options.client_pid(value.parse().map_err(Error::config)?),
119                "hostname" => options = options.hostname(&*value),
120                "app_name" => options = options.app_name(&*value),
121                "server_name" => options = options.server_name(&*value),
122                "client_interface_name" => options = options.client_interface_name(&*value),
123                "language" => options = options.language(&*value),
124                _ => {
125                    return Err(Error::config(MssqlInvalidOption(key.into())));
126                }
127            }
128        }
129        Ok(options)
130    }
131}
132
133#[derive(Debug)]
134struct MssqlInvalidOption(String);
135
136impl std::fmt::Display for MssqlInvalidOption {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        write!(f, "`{}` is not a valid mssql connection option", self.0)
139    }
140}
141
142impl std::error::Error for MssqlInvalidOption {}
143
144#[test]
145fn it_parses_username_with_at_sign_correctly() {
146    let url = "mssql://user@hostname:password@hostname:5432/database";
147    let opts = MssqlConnectOptions::from_str(url).unwrap();
148
149    assert_eq!("user@hostname", &opts.username);
150}
151
152#[test]
153fn it_parses_password_with_non_ascii_chars_correctly() {
154    let url = "mssql://username:p@ssw0rd@hostname:5432/database";
155    let opts = MssqlConnectOptions::from_str(url).unwrap();
156
157    assert_eq!(Some("p@ssw0rd".into()), opts.password);
158}