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¶m2=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}