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    /// - `sslrootcert` or `ssl-root-cert` or `ssl-ca`: Path to the root certificate for validating the server's SSL certificate.
32    /// - `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.
33    /// - `hostname_in_certificate`: The hostname expected in the server's SSL certificate. Use this when the server's hostname doesn't match the certificate.
34    /// - `packet_size`: Size of TDS packets in bytes. Larger sizes can improve performance but consume more memory on the server
35    /// - `client_program_version`: Version number of the client program, sent to the server for logging purposes.
36    /// - `client_pid`: Process ID of the client, sent to the server for logging purposes.
37    /// - `hostname`: Name of the client machine, sent to the server for logging purposes.
38    /// - `app_name`: Name of the client application, sent to the server for logging purposes.
39    /// - `server_name`: Name of the server to connect to. Useful when connecting through a proxy or load balancer.
40    /// - `client_interface_name`: Name of the client interface, sent to the server for logging purposes.
41    /// - `language`: Sets the language for server messages. Affects date formats and system messages.
42    ///
43    /// Example:
44    /// ```text
45    /// mssql://user:pass@localhost:1433/mydb?encrypt=strict&app_name=MyApp&packet_size=4096
46    /// ```
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        let url: Url = s.parse().map_err(Error::config)?;
49        let mut options = Self::new();
50
51        if let Some(host) = url.host_str() {
52            options = options.host(host);
53        }
54
55        if let Some(port) = url.port() {
56            options = options.port(port);
57        }
58
59        let username = url.username();
60        if !username.is_empty() {
61            options = options.username(
62                &*percent_decode_str(username)
63                    .decode_utf8()
64                    .map_err(Error::config)?,
65            );
66        }
67
68        if let Some(password) = url.password() {
69            options = options.password(
70                &*percent_decode_str(password)
71                    .decode_utf8()
72                    .map_err(Error::config)?,
73            );
74        }
75
76        let path = url.path().trim_start_matches('/');
77        if !path.is_empty() {
78            options = options.database(path);
79        }
80
81        for (key, value) in url.query_pairs() {
82            match key.as_ref() {
83                "instance" => {
84                    options = options.instance(&*value);
85                }
86                "encrypt" => {
87                    match value.to_lowercase().as_str() {
88                        "strict" => options = options.encrypt(Encrypt::Required),
89                        "mandatory" | "true" | "yes" => options = options.encrypt(Encrypt::On),
90                        "optional" | "false" | "no" => options = options.encrypt(Encrypt::Off),
91                        "not_supported" => options = options.encrypt(Encrypt::NotSupported),
92                        _ => return Err(Error::config(MssqlInvalidOption(format!(
93                            "encrypt={} is not a valid value for encrypt. Valid values are: strict, mandatory, optional, true, false, yes, no",
94                            value
95                        )))),
96                    }
97                }
98                "sslrootcert" | "ssl-root-cert" | "ssl-ca" => {
99                    options = options.ssl_root_cert(&*value);
100                }
101                "trust_server_certificate" => {
102                    let trust = value.parse::<bool>().map_err(Error::config)?;
103                    options = options.trust_server_certificate(trust);
104                }
105                "hostname_in_certificate" => {
106                    options = options.hostname_in_certificate(&*value);
107                }
108                "packet_size" => {
109                    let size = value.parse().map_err(Error::config)?;
110                    options = options.requested_packet_size(size).map_err(|_| {
111                        Error::config(MssqlInvalidOption(format!("packet_size={}", size)))
112                    })?;
113                }
114                "client_program_version" => {
115                    options = options.client_program_version(value.parse().map_err(Error::config)?)
116                }
117                "client_pid" => options = options.client_pid(value.parse().map_err(Error::config)?),
118                "hostname" => options = options.hostname(&*value),
119                "app_name" => options = options.app_name(&*value),
120                "server_name" => options = options.server_name(&*value),
121                "client_interface_name" => options = options.client_interface_name(&*value),
122                "language" => options = options.language(&*value),
123                _ => {
124                    return Err(Error::config(MssqlInvalidOption(key.into())));
125                }
126            }
127        }
128        Ok(options)
129    }
130}
131
132#[derive(Debug)]
133struct MssqlInvalidOption(String);
134
135impl std::fmt::Display for MssqlInvalidOption {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "`{}` is not a valid mssql connection option", self.0)
138    }
139}
140
141impl std::error::Error for MssqlInvalidOption {}
142
143#[test]
144fn it_parses_username_with_at_sign_correctly() {
145    let url = "mysql://user@hostname:password@hostname:5432/database";
146    let opts = MssqlConnectOptions::from_str(url).unwrap();
147
148    assert_eq!("user@hostname", &opts.username);
149}
150
151#[test]
152fn it_parses_password_with_non_ascii_chars_correctly() {
153    let url = "mysql://username:p@ssw0rd@hostname:5432/database";
154    let opts = MssqlConnectOptions::from_str(url).unwrap();
155
156    assert_eq!(Some("p@ssw0rd".into()), opts.password);
157}